From cd34fd04918912ec49f9ff53aae8162269e967c0 Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Mon, 17 Mar 2025 15:51:42 +0100 Subject: [PATCH 01/28] test: initial version of framework and some first tests --- .../distribution_function/mod.rs | 2 + .../distribution/perpetual/block_based.rs | 763 ++++++++++++++++++ 2 files changed, 765 insertions(+) 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/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..6604c55d5db 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 @@ -18,6 +18,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(); @@ -494,3 +495,765 @@ mod perpetual_distribution_block { assert_eq!(token_balance, Some(200)); } } + +#[cfg(test)] +mod block_based_test_suite_tests { + use dpp::consensus::state::state_error::StateError; + use dpp::consensus::ConsensusError; + 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::TokenPerpetualDistribution; + use dpp::data_contract::associated_token::token_perpetual_distribution::v0::TokenPerpetualDistributionV0; + use dpp::data_contract::TokenConfiguration; + use rust_decimal::prelude::ToPrimitive; + use crate::platform_types::state_transitions_processing_result::StateTransitionExecutionResult; + + use super::test_suite::*; + + // Given some token configuration, + // When a claim is made at block 42, + // Then the claim should be successful. + #[test] + + fn test_block_based_perpetual_fixed_amount_50() { + check_heights_odd_no_current_rewards( + DistributionFunction::FixedAmount { amount: 50 }, + &[41, 46, 50, 1000], + &[100200, 100200, 100250], + 10, + ) + .expect("\n-> fixed amount should pass"); + } + + /// Test case for overflow error. + /// + /// claim at height 1000000000000: claim failed: assertion 0 failed: expected SuccessfulExecution, + /// got [InternalError(\"storage: protocol: overflow error: Overflow in FixedAmount evaluation\")]" + #[test] + fn test_block_based_perpetual_fixed_amount_1_000_000_000() { + check_heights_odd_no_current_rewards( + DistributionFunction::FixedAmount { + amount: 1_000_000_000, + }, + &[41, 46, 50, 51, 1_000_000_000_000], + &[ + 100_000 + 4 * 1_000_000_000, + 100_000 + 4 * 1_000_000_000, + 100_000 + 5 * 1_000_000_000, + 100_000 + 5 * 1_000_000_000, + 1, // 100_000 + (1_000_000_000_000 / 10) * 1_000_000_000, -- this will overflow + ], + 10, + ) + .expect("\n-> fixed amount should pass"); + } + + #[test] + /// With a fixed amount of 0, we expect first claim to fetch 100_000 units (which are in the contract defintion), + /// and fail for the rest of the claims. + /// + /// FAILS + fn test_block_based_perpetual_fixed_amount_0() { + check_heights( + DistributionFunction::FixedAmount { amount: 0 }, + &[41, 46, 50, 100000], + &[100000, 100000, 100000, 100000], + &[true, false, false, false], + None, + 10, + ) + .expect("\nfixed amount zero increase\n"); + } + + #[test] + fn test_block_based_perpetual_fixed_amount_u64_max() { + check_heights_odd_no_current_rewards( + DistributionFunction::FixedAmount { amount: u64::MAX }, + &[41, 46, 50, 1000], + &[100200, 100200, 100250, 100250], + 10, + ) + .expect("\nfixed amount u64::MAX should pass\n"); + } + + #[test] + fn test_block_based_perpetual_random() { + check_heights_odd_no_current_rewards( + DistributionFunction::Random { min: 0, max: 100 }, + &[41, 46, 50, 59, 60], + &[100192, 100192, 100263, 100263, 100310], + 10, + ) + .expect("correct case 1"); + + check_heights_odd_no_current_rewards( + DistributionFunction::Random { min: 0, max: 0 }, + &[41], + &[100192], + 10, + ) + .expect("no rewards"); + } + + /// Test [DistributionFunction::StepDecreasingAmount]. + #[test] + fn test_block_based_perpetual_step_decreasing() { + struct Case<'a> { + name: String, + dist_function: DistributionFunction, + claim_heights: &'a [u64], + distribution_interval: u64, + } + let claim_heights = [1, 2, 3, 4, 5, 10, 20, 30, 50, 100, 1000, 1000000]; + + let test_cases = [ + Case { + name: "claim height u64::MAX".to_string(), + dist_function: DistributionFunction::StepDecreasingAmount { + step_count: 10, + decrease_per_interval_numerator: 1, + decrease_per_interval_denominator: 1, + s: Some(1), + n: 100_000, + min_value: Some(1), + }, + claim_heights: &[u64::MAX], + distribution_interval: 10, + }, + Case { + name: "no change".to_string(), + dist_function: DistributionFunction::StepDecreasingAmount { + step_count: 10, + decrease_per_interval_numerator: 1, + decrease_per_interval_denominator: 1, + s: Some(1), + n: 100_000, + min_value: Some(1), + }, + claim_heights: &claim_heights, + distribution_interval: 10, + }, + Case { + name: "increase by u16::MAX".to_string(), + dist_function: DistributionFunction::StepDecreasingAmount { + step_count: 10, + decrease_per_interval_numerator: u16::MAX, + decrease_per_interval_denominator: 1, + s: Some(1), + n: 100_000, + min_value: Some(1), + }, + claim_heights: &claim_heights, + distribution_interval: 10, + }, + Case { + name: "zero decrease".to_string(), + dist_function: DistributionFunction::StepDecreasingAmount { + step_count: 10, + decrease_per_interval_numerator: 0, + decrease_per_interval_denominator: 1, + s: Some(1), + n: 100_000, + min_value: Some(1), + }, + claim_heights: &claim_heights, + distribution_interval: 10, + }, + Case { + name: "divide by 0".to_string(), + dist_function: DistributionFunction::StepDecreasingAmount { + step_count: 10, + decrease_per_interval_numerator: 0, + decrease_per_interval_denominator: 1, + s: Some(1), + n: 100_000, + min_value: Some(1), + }, + claim_heights: &claim_heights, + distribution_interval: 10, + }, + Case { + name: "decrease by 10%".to_string(), + dist_function: DistributionFunction::StepDecreasingAmount { + step_count: 10, + decrease_per_interval_numerator: 1, + decrease_per_interval_denominator: 10, + s: Some(1), + n: 100_000, + min_value: Some(1), + }, + claim_heights: &claim_heights, + distribution_interval: 10, + }, + Case { + name: "decrease by 50%".to_string(), + dist_function: DistributionFunction::StepDecreasingAmount { + step_count: 10, + decrease_per_interval_numerator: 1, + decrease_per_interval_denominator: 2, + s: Some(1), + n: 100_000, + min_value: Some(1), + }, + claim_heights: &claim_heights, + distribution_interval: 10, + }, + Case { + name: "increase by 50%".to_string(), + dist_function: DistributionFunction::StepDecreasingAmount { + step_count: 10, + decrease_per_interval_numerator: 2, + decrease_per_interval_denominator: 1, + s: Some(1), + n: 100_000, + min_value: Some(1), + }, + claim_heights: &claim_heights, + distribution_interval: 10, + }, + { + Case { + name: "decrease by 90%".to_string(), + dist_function: DistributionFunction::StepDecreasingAmount { + step_count: 10, + decrease_per_interval_numerator: 9, + decrease_per_interval_denominator: 10, + s: Some(1), + n: 100_000, + min_value: Some(1), + }, + claim_heights: &claim_heights, + distribution_interval: 10, + } + }, + { + Case { + name: "decrease by 99%".to_string(), + dist_function: DistributionFunction::StepDecreasingAmount { + step_count: 10, + decrease_per_interval_numerator: 99, + decrease_per_interval_denominator: 100, + s: Some(1), + n: 100, + min_value: Some(1), + }, + claim_heights: &claim_heights, + distribution_interval: 10, + } + }, + ]; + + // f(x) = n * (1 - (decrease_per_interval_numerator / decrease_per_interval_denominator))^((x - s) / step_count) + fn expected_emission(x: u64, dist: &DistributionFunction) -> u64 { + 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, + *decrease_per_interval_numerator, + *decrease_per_interval_denominator, + s.unwrap_or(1), + *n, + min_value.unwrap_or_default(), + ), + _ => panic!("expected StepDecreasingAmount"), + }; + + if x <= s { + return n; + } + + // 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 as f64 - s as f64) as i32 / step_count as i32; // integer by purpose, we want to round down + let f_x = n as f64 * a.powi(b); + + // println!("expected_emission({}) = {}", x, f_x); + // f_x.to_u64().expect("expected to convert to u64") + f_x.to_u64().unwrap_or(min_value).max(min_value) + } + + let mut fails = String::new(); + + for case in test_cases { + println!("TEST CASE '{}'", case.name); + let dist = case.dist_function; + let claim_heights = case.claim_heights; + let expected_balances = claim_heights + .iter() + .map(|&h| { + // initial balance, defined in contract js + let mut expected_balance = 100_000; + // loop over blocks, starting with S, with step PERPETUAL_DISTRIBUTION_INTERVAL + for i in (1..=h).step_by(case.distribution_interval as usize) { + expected_balance += expected_emission(i, &dist); + } + println!("expected balance at height {}: {}", h, expected_balance); + expected_balance + }) + .collect::>(); + // we expect all tests to pass + let expect_pass = claim_heights.iter().map(|&_h| true).collect::>(); + + if let Err(e) = check_heights( + dist, + claim_heights, + &expected_balances, + &expect_pass, + None, //Some(S), + case.distribution_interval, + ) { + fails.push_str(format!("-> Test '{}':\n{}\n", case.name, &e).as_str()); + } + } + + if !fails.is_empty() { + panic!("failed tests:\n{}", fails); + } + } + + /// Check that claim results at provided heights are as expected, and that balances match expectations. + fn check_heights( + distribution_function: DistributionFunction, + claim_heights: &[u64], + expected_balances: &[u64], + expect_pass: &[bool], + contract_start_height: Option, + distribution_interval: u64, + ) -> Result<(), String> { + let mut suite = TestSuite::new( + 10_200_000_000, + 0, + TokenDistributionType::Perpetual, + Some(|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(start) = contract_start_height { + suite = suite.with_contract_start_time(start); + } + + let mut tests = Vec::new(); + for (i, height) in claim_heights.iter().enumerate() { + let assertions: Vec = if expect_pass[i] { + vec![|processing_results: &[_]| match processing_results { + [StateTransitionExecutionResult::SuccessfulExecution(_, _)] => Ok(()), + _ => Err(format!( + "expected SuccessfulExecution, got {:?}", + processing_results + )), + }] + } else { + vec![|processing_results: &[_]| match processing_results { + [StateTransitionExecutionResult::SuccessfulExecution(_, _)] => { + Err("expected error, got SuccessfulExecution".into()) + } + _ => Ok(()), + }] + }; + + tests.push(TestCase { + name: format!("claim at height {}", height), + base_height: *height - 1, + base_time_ms: 10_200_000_000, + expected_balance: expected_balances[i], + assertions, + }); + } + + suite.execute(&tests) + } + /// This test checks claims at provided heights, where every second height does not have any rewards to claim. + /// + /// # Arguments + /// + /// * `distribution_function` - configured distribution function to test + /// * `claim_heights` - heights at which claims will be made; they will see balance from previous height + /// * `expected_balances` - expected balances after claims were made and block from `heights` was committed + /// + fn check_heights_odd_no_current_rewards( + distribution_function: DistributionFunction, + claim_heights: &[u64], + expected_balances: &[u64], + distribution_interval: u64, + ) -> Result<(), String> { + let mut suite = TestSuite::new( + 10_200_000_000, + 0, + TokenDistributionType::Perpetual, + Some(|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, + }, + ))); + }), + ); + + let mut tests = Vec::new(); + for (i, height) in claim_heights.iter().enumerate() { + let assertions: Vec = if i % 2 == 0 { + vec![|processing_results: &[_]| match processing_results { + [StateTransitionExecutionResult::SuccessfulExecution(_, _)] => Ok(()), + _ => Err(format!( + "expected SuccessfulExecution, got {:?}", + processing_results + )), + }] + } else { + vec![|processing_results: &[_]| match processing_results { + [StateTransitionExecutionResult::PaidConsensusError( + ConsensusError::StateError(StateError::InvalidTokenClaimNoCurrentRewards( + _, + )), + _, + )] => Ok(()), + _ => Err(format!( + "expected InvalidTokenClaimNoCurrentRewards, got {:?}", + processing_results + )), + }] + }; + + tests.push(TestCase { + name: format!("claim at height {}", height), + base_height: *height - 1, + base_time_ms: 10_200_000_000, + expected_balance: expected_balances[i], + assertions, + }); + } + + suite.execute(&tests) + } +} + +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::prelude::{DataContract, IdentityPublicKey}; + use simple_signer::signer::SimpleSigner; + + 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, + } + + impl TestSuite { + 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(); + + 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)); + + 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, + } + .with_genesis(1, genesis_time_ms) + } + /// 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| { + tc(token_configuration); + }; + 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 assert_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( + &vec![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(()) + } + + pub(crate) fn assert_balance( + &mut self, + expected_balance: Option, + ) -> Result<(), String> { + let token_id = self.get_token_id().to_buffer(); + let token_balance = self + .platform + .drive + .fetch_identity_token_balance( + token_id, + self.identity.id().to_buffer(), + None, + self.platform_version, + ) + .expect("expected to fetch token 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: u64) -> 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 + } + /// execute test cases + pub(super) fn execute(&mut self, tests: &[TestCase]) -> Result<(), String> { + let mut errors = String::new(); + for test_case in tests { + let result = self.execute_test_case(test_case); + if let Err(e) = result { + errors += format!("\n--> {}: {}", test_case.name, e).as_str(); + } + } + + if errors.is_empty() { + Ok(()) + } else { + Err(errors) + } + } + + pub(super) fn execute_test_case(&mut self, test_case: &TestCase) -> 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, + ); + self.assert_claim(test_case.assertions.clone()) + .map_err(|e| format!("claim failed: {}", e))?; + self.assert_balance(Some(test_case.expected_balance)) + .map_err(|e| format!("invalid balance: {}", e))?; + + Ok(()) + } + } + + pub(crate) type AssertionFn = fn(&[StateTransitionExecutionResult]) -> Result<(), String>; + pub(crate) struct TestCase { + 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: u64, + /// assertion functions that will be executed on the claim + pub(crate) assertions: Vec, + } +} From 2c338d62a0643d039faa0ff1dcba8a023a66c427 Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Tue, 18 Mar 2025 15:07:18 +0100 Subject: [PATCH 02/28] WIP --- Cargo.lock | 1 + packages/rs-drive-abci/Cargo.toml | 1 + .../distribution/perpetual/block_based.rs | 111 +++++++++++------- 3 files changed, 68 insertions(+), 45 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0fb070fb49d..a1eb8fe3328 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1611,6 +1611,7 @@ dependencies = [ "strategy-tests", "tempfile", "tenderdash-abci", + "test-case", "thiserror 1.0.64", "tokio", "tokio-util", diff --git a/packages/rs-drive-abci/Cargo.toml b/packages/rs-drive-abci/Cargo.toml index 33ade76e797..6c315df9677 100644 --- a/packages/rs-drive-abci/Cargo.toml +++ b/packages/rs-drive-abci/Cargo.toml @@ -103,6 +103,7 @@ 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" } +test-case = { version = "3.3.1" } # For tests of grovedb verify rocksdb = { version = "0.23.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 6604c55d5db..3b33624de85 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 @@ -498,6 +498,7 @@ mod perpetual_distribution_block { #[cfg(test)] mod block_based_test_suite_tests { + use dpp::balances::credits::TokenAmount; use dpp::consensus::state::state_error::StateError; use dpp::consensus::ConsensusError; use dpp::data_contract::associated_token::token_configuration::accessors::v0::TokenConfigurationV0Getters; @@ -510,6 +511,7 @@ mod block_based_test_suite_tests { use dpp::data_contract::associated_token::token_perpetual_distribution::v0::TokenPerpetualDistributionV0; use dpp::data_contract::TokenConfiguration; use rust_decimal::prelude::ToPrimitive; + use test_case::test_matrix; use crate::platform_types::state_transitions_processing_result::StateTransitionExecutionResult; use super::test_suite::*; @@ -599,6 +601,26 @@ mod block_based_test_suite_tests { .expect("no rewards"); } + #[test_case::test_matrix( + [1,10], // step_count + [0,1,u16::MAX,999], // decrease_per_interval_numerator + [0,1,2,10,100,u16::MAX], // decrease_per_interval_denominator + [None,Some(1),Some(10),Some(u64::MAX)], // s + [0,1,100,100_000, 1_000_000, 10_000_000, 100_000_000, u64::MAX], // n + [None,Some(1),Some(10),Some(u64::MAX)], // min_value + [0,110, 100, 1000] // distribution_interval + )] + fn test_block_based_perpetual_step_decreasing_matrix( + step_count: u32, + decrease_per_interval_numerator: u16, + decrease_per_interval_denominator: u16, + s: Option, + n: TokenAmount, + min_value: Option, + + distribution_interval: u64, + ) { + } /// Test [DistributionFunction::StepDecreasingAmount]. #[test] fn test_block_based_perpetual_step_decreasing() { @@ -747,51 +769,6 @@ mod block_based_test_suite_tests { }, ]; - // f(x) = n * (1 - (decrease_per_interval_numerator / decrease_per_interval_denominator))^((x - s) / step_count) - fn expected_emission(x: u64, dist: &DistributionFunction) -> u64 { - 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, - *decrease_per_interval_numerator, - *decrease_per_interval_denominator, - s.unwrap_or(1), - *n, - min_value.unwrap_or_default(), - ), - _ => panic!("expected StepDecreasingAmount"), - }; - - if x <= s { - return n; - } - - // 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 as f64 - s as f64) as i32 / step_count as i32; // integer by purpose, we want to round down - let f_x = n as f64 * a.powi(b); - - // println!("expected_emission({}) = {}", x, f_x); - // f_x.to_u64().expect("expected to convert to u64") - f_x.to_u64().unwrap_or(min_value).max(min_value) - } - let mut fails = String::new(); for case in test_cases { @@ -830,7 +807,51 @@ mod block_based_test_suite_tests { panic!("failed tests:\n{}", fails); } } + // HELPER FUNCTIONS // + + // f(x) = n * (1 - (decrease_per_interval_numerator / decrease_per_interval_denominator))^((x - s) / step_count) + fn expected_emission(x: u64, dist: &DistributionFunction) -> u64 { + 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, + *decrease_per_interval_numerator, + *decrease_per_interval_denominator, + s.unwrap_or(1), + *n, + min_value.unwrap_or_default(), + ), + _ => panic!("expected StepDecreasingAmount"), + }; + + if x <= s { + return n; + } + + // 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 as f64 - s as f64) as i32 / step_count as i32; // integer by purpose, we want to round down + let f_x = n as f64 * a.powi(b); + // println!("expected_emission({}) = {}", x, f_x); + // f_x.to_u64().expect("expected to convert to u64") + f_x.to_u64().unwrap_or(min_value).max(min_value) + } /// Check that claim results at provided heights are as expected, and that balances match expectations. fn check_heights( distribution_function: DistributionFunction, From 938fea9791bd2dd3d1be82474179a0960eb36113 Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Thu, 20 Mar 2025 16:43:58 +0100 Subject: [PATCH 03/28] chore: test decreasing distribution --- .../distribution/perpetual/block_based.rs | 536 ++++++++++-------- 1 file changed, 300 insertions(+), 236 deletions(-) diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/perpetual/block_based.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/perpetual/block_based.rs index 3b33624de85..2cc16b5d1e7 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 @@ -497,24 +497,9 @@ mod perpetual_distribution_block { } #[cfg(test)] -mod block_based_test_suite_tests { - use dpp::balances::credits::TokenAmount; - use dpp::consensus::state::state_error::StateError; - use dpp::consensus::ConsensusError; - 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::TokenPerpetualDistribution; - use dpp::data_contract::associated_token::token_perpetual_distribution::v0::TokenPerpetualDistributionV0; - use dpp::data_contract::TokenConfiguration; - use rust_decimal::prelude::ToPrimitive; - use test_case::test_matrix; - use crate::platform_types::state_transitions_processing_result::StateTransitionExecutionResult; - +mod block_based_perpetual_fixed_amount { use super::test_suite::*; + use dpp::data_contract::associated_token::token_perpetual_distribution::distribution_function::DistributionFunction; // Given some token configuration, // When a claim is made at block 42, @@ -522,7 +507,7 @@ mod block_based_test_suite_tests { #[test] fn test_block_based_perpetual_fixed_amount_50() { - check_heights_odd_no_current_rewards( + super::test_suite::check_heights_odd_no_current_rewards( DistributionFunction::FixedAmount { amount: 50 }, &[41, 46, 50, 1000], &[100200, 100200, 100250], @@ -581,6 +566,11 @@ mod block_based_test_suite_tests { ) .expect("\nfixed amount u64::MAX should pass\n"); } +} +mod block_based_perpetual_random { + use dpp::data_contract::associated_token::token_perpetual_distribution::distribution_function::DistributionFunction; + + use super::test_suite::check_heights_odd_no_current_rewards; #[test] fn test_block_based_perpetual_random() { @@ -600,14 +590,32 @@ mod block_based_test_suite_tests { ) .expect("no rewards"); } +} +mod matrix { + use dpp::{ + balances::credits::TokenAmount, + data_contract::associated_token::token_perpetual_distribution::distribution_function::DistributionFunction, + }; + use rust_decimal::prelude::ToPrimitive; + use crate::execution::validation::state_transition::batch::tests::token::distribution::perpetual::block_based::{block_based_perpetual_step_decreasing::expected_emission, test_suite::check_heights}; + + // #[test_case::test_matrix( + // [1,10], // step_count + // [0,1,u16::MAX,999], // decrease_per_interval_numerator + // [0,1,2,10,100,u16::MAX], // decrease_per_interval_denominator + // [None,Some(1),Some(10),Some(u64::MAX)], // s + // [0,1,100,100_000, 1_000_000, 10_000_000, 100_000_000, u64::MAX], // n + // [None,Some(1),Some(10),Some(u64::MAX)], // min_value + // [0,110, 100, 1000] // distribution_interval + // )] #[test_case::test_matrix( - [1,10], // step_count - [0,1,u16::MAX,999], // decrease_per_interval_numerator - [0,1,2,10,100,u16::MAX], // decrease_per_interval_denominator - [None,Some(1),Some(10),Some(u64::MAX)], // s - [0,1,100,100_000, 1_000_000, 10_000_000, 100_000_000, u64::MAX], // n - [None,Some(1),Some(10),Some(u64::MAX)], // min_value + [0,1,10], // step_count + [0,1,10,u16::MAX], // decrease_per_interval_numerator + [1], // decrease_per_interval_denominator + [None], // s + [0,1,100000], // n + [None], // min_value [0,110, 100, 1000] // distribution_interval )] fn test_block_based_perpetual_step_decreasing_matrix( @@ -617,200 +625,227 @@ mod block_based_test_suite_tests { s: Option, n: TokenAmount, min_value: Option, - distribution_interval: u64, ) { - } - /// Test [DistributionFunction::StepDecreasingAmount]. - #[test] - fn test_block_based_perpetual_step_decreasing() { - struct Case<'a> { - name: String, - dist_function: DistributionFunction, - claim_heights: &'a [u64], - distribution_interval: u64, - } - let claim_heights = [1, 2, 3, 4, 5, 10, 20, 30, 50, 100, 1000, 1000000]; - - let test_cases = [ - Case { - name: "claim height u64::MAX".to_string(), - dist_function: DistributionFunction::StepDecreasingAmount { - step_count: 10, - decrease_per_interval_numerator: 1, - decrease_per_interval_denominator: 1, - s: Some(1), - n: 100_000, - min_value: Some(1), - }, - claim_heights: &[u64::MAX], - distribution_interval: 10, - }, - Case { - name: "no change".to_string(), - dist_function: DistributionFunction::StepDecreasingAmount { - step_count: 10, - decrease_per_interval_numerator: 1, - decrease_per_interval_denominator: 1, - s: Some(1), - n: 100_000, - min_value: Some(1), - }, - claim_heights: &claim_heights, - distribution_interval: 10, - }, - Case { - name: "increase by u16::MAX".to_string(), - dist_function: DistributionFunction::StepDecreasingAmount { - step_count: 10, - decrease_per_interval_numerator: u16::MAX, - decrease_per_interval_denominator: 1, - s: Some(1), - n: 100_000, - min_value: Some(1), - }, - claim_heights: &claim_heights, - distribution_interval: 10, - }, - Case { - name: "zero decrease".to_string(), - dist_function: DistributionFunction::StepDecreasingAmount { - step_count: 10, - decrease_per_interval_numerator: 0, - decrease_per_interval_denominator: 1, - s: Some(1), - n: 100_000, - min_value: Some(1), - }, - claim_heights: &claim_heights, - distribution_interval: 10, - }, - Case { - name: "divide by 0".to_string(), - dist_function: DistributionFunction::StepDecreasingAmount { - step_count: 10, - decrease_per_interval_numerator: 0, - decrease_per_interval_denominator: 1, - s: Some(1), - n: 100_000, - min_value: Some(1), - }, - claim_heights: &claim_heights, - distribution_interval: 10, - }, - Case { - name: "decrease by 10%".to_string(), - dist_function: DistributionFunction::StepDecreasingAmount { - step_count: 10, - decrease_per_interval_numerator: 1, - decrease_per_interval_denominator: 10, - s: Some(1), - n: 100_000, - min_value: Some(1), - }, - claim_heights: &claim_heights, - distribution_interval: 10, - }, - Case { - name: "decrease by 50%".to_string(), - dist_function: DistributionFunction::StepDecreasingAmount { - step_count: 10, - decrease_per_interval_numerator: 1, - decrease_per_interval_denominator: 2, - s: Some(1), - n: 100_000, - min_value: Some(1), - }, - claim_heights: &claim_heights, - distribution_interval: 10, - }, - Case { - name: "increase by 50%".to_string(), - dist_function: DistributionFunction::StepDecreasingAmount { - step_count: 10, - decrease_per_interval_numerator: 2, - decrease_per_interval_denominator: 1, - s: Some(1), - n: 100_000, - min_value: Some(1), - }, - claim_heights: &claim_heights, - distribution_interval: 10, - }, - { - Case { - name: "decrease by 90%".to_string(), - dist_function: DistributionFunction::StepDecreasingAmount { - step_count: 10, - decrease_per_interval_numerator: 9, - decrease_per_interval_denominator: 10, - s: Some(1), - n: 100_000, - min_value: Some(1), - }, - claim_heights: &claim_heights, - distribution_interval: 10, - } - }, - { - Case { - name: "decrease by 99%".to_string(), - dist_function: DistributionFunction::StepDecreasingAmount { - step_count: 10, - decrease_per_interval_numerator: 99, - decrease_per_interval_denominator: 100, - s: Some(1), - n: 100, - min_value: Some(1), - }, - claim_heights: &claim_heights, - distribution_interval: 10, - } - }, - ]; - - let mut fails = String::new(); - - for case in test_cases { - println!("TEST CASE '{}'", case.name); - let dist = case.dist_function; - let claim_heights = case.claim_heights; - let expected_balances = claim_heights - .iter() - .map(|&h| { - // initial balance, defined in contract js - let mut expected_balance = 100_000; - // loop over blocks, starting with S, with step PERPETUAL_DISTRIBUTION_INTERVAL - for i in (1..=h).step_by(case.distribution_interval as usize) { + // + let dist = DistributionFunction::StepDecreasingAmount { + step_count, + decrease_per_interval_numerator, + decrease_per_interval_denominator, + s, + n, + min_value, + }; + + const VERY_HIGH_HEIGHT: u64 = 1_000_000; + let claim_heights = if distribution_interval > 0 { + let mut heights = (1..10) + .map(|i| i * distribution_interval) + .collect::>(); + heights.push(VERY_HIGH_HEIGHT); + + heights + } else { + vec![1, 2, 3, 10, 100, VERY_HIGH_HEIGHT] + }; + let expected_balances = claim_heights + .iter() + .map(|&h| { + // initial balance, defined in contract js + let mut expected_balance = 100_000; + // loop over blocks, starting with S, with step PERPETUAL_DISTRIBUTION_INTERVAL + if distribution_interval > 0 { + for i in (1..=h).step_by(distribution_interval as usize) { expected_balance += expected_emission(i, &dist); } - println!("expected balance at height {}: {}", h, expected_balance); - expected_balance - }) - .collect::>(); - // we expect all tests to pass - let expect_pass = claim_heights.iter().map(|&_h| true).collect::>(); + } + println!("expected balance at height {}: {}", h, expected_balance); + expected_balance.to_u64().unwrap_or(0) // to handle tests that overflow + }) + .collect::>(); + // we expect all tests to pass + let expect_pass = claim_heights.iter().map(|&_h| true).collect::>(); + + if let Err(e) = check_heights( + dist.clone(), + &claim_heights, + &expected_balances, + &expect_pass, + None, //Some(S), + distribution_interval, + ) { + // print dist to stderr + panic!("test failed for distribution function {:?}: {}", dist, e); + } else { + println!("test passed for distribution function {:?}", dist); + } + } +} + +mod block_based_perpetual_step_decreasing { + use dpp::balances::credits::TokenAmount; + use dpp::data_contract::associated_token::token_perpetual_distribution::distribution_function::DistributionFunction; + use rust_decimal::prelude::ToPrimitive; + use test_case::test_case; + use crate::execution::validation::state_transition::batch::tests::token::distribution::perpetual::block_based::test_suite::check_heights; + use super::test_suite::with_timeout; + + const TIMEOUT: tokio::time::Duration = tokio::time::Duration::from_secs(1); + + #[test_case( + 1,// step_count + 1,// decrease_per_interval_numerator + 100,// decrease_per_interval_denominator + None,// s + 100_000,// n + Some(1),// min_value + Some((1..1000).step_by(100).collect()),// claim_heights + 1; // distribution_interval + "claim every 100 blocks" + )] + #[test_case( + 1,// step_count + 1,// decrease_per_interval_numerator + 100,// decrease_per_interval_denominator + None,// s + 100_000,// n + Some(1),// min_value + Some((1..1000).step_by(500).collect()),// claim_heights + 1; // distribution_interval + "claim every 500 blocks" + )] + #[test_matrix( + 1,// step_count + 101,// decrease_per_interval_numerator + 100,// decrease_per_interval_denominator + None,// s + 100_000,// n + Some(1),// min_value + [Some((1..1000).step_by(100).collect()),Some((1..1000).step_by(500).collect())],// claim_heights + 1; // distribution_interval + "1% increase, varying claim heights" + )] + #[test_case( + 1,// step_count + 1000,// decrease_per_interval_numerator + 1,// decrease_per_interval_denominator + None,// s + 100_000,// n + Some(1),// min_value + Some(vec![1,7]), // claim_heights + 1; // distribution_interval + "1000x increase, overflow" + )] + #[test_case( + 1,// step_count + 1,// decrease_per_interval_numerator + 1,// decrease_per_interval_denominator + None,// s + 100_000,// n + Some(1),// min_value + Some(vec![1,2,3,10,100]), // claim_heights // ,300,500,800,1_000,1_000_000 + 1; // distribution_interval + "100% decrease, various min values" + )] + #[test_matrix( + 1,// step_count + 0,// decrease_per_interval_numerator + 1,// decrease_per_interval_denominator + None,// s + 100_000,// n + [None,Some(0),Some(1),Some(100)],// min_value + Some(vec![1,2,3,10,100]), // claim_heights // ,300,500,800,1_000,1_000_000 + 1; // distribution_interval + "no decrease, irrelevant min values" + )] + #[test_matrix( + [5,10],// step_count + 1,// decrease_per_interval_numerator + 2,// decrease_per_interval_denominator + None,// s + 100_000,// n + None,// min_value + Some(vec![5,10,100]), // claim_heights // ,300,500,800,1_000,1_000_000 + [1,5]; // distribution_interval + "1/2 decrease, changing step" + )] + #[test_matrix( + [1,10],// step_count + 1,// decrease_per_interval_numerator + 2,// decrease_per_interval_denominator + [None,Some(1),Some(5)],// s + 100_000,// n + None,// min_value + Some(vec![5,10,100]), // claim_heights // ,300,500,800,1_000,1_000_000 + [1,5]; // distribution_interval + "1/2 decrease, changing S" + )] - if let Err(e) = check_heights( + /// Test various combinations of [DistributionFunction::StepDecreasingAmount] distribution. + 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 = 100_000; + // 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); + } + println!("expected balance at height {}: {}", h, expected_balance); + expected_balance.to_u64().unwrap_or_else(|| { + println!("ERR: overflow in expected balance at height {}", h); + 0 + }) // to handle tests that overflow + }) + .collect::>(); + // we expect all tests to pass + let expect_pass = claim_heights.iter().map(|&_h| true).collect::>(); + + with_timeout(TIMEOUT, move || { + check_heights( dist, - claim_heights, + &claim_heights, &expected_balances, &expect_pass, None, //Some(S), - case.distribution_interval, - ) { - fails.push_str(format!("-> Test '{}':\n{}\n", case.name, &e).as_str()); - } - } - - if !fails.is_empty() { - panic!("failed tests:\n{}", fails); - } + distribution_interval, + ) + }) + .inspect_err(|e| { + println!("{}", e); + }) } - // HELPER FUNCTIONS // + // ===== 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) - fn expected_emission(x: u64, dist: &DistributionFunction) -> u64 { + pub(super) fn expected_emission(x: u64, dist: &DistributionFunction) -> i128 { + let x = x as i128; let ( step_count, decrease_per_interval_numerator, @@ -827,33 +862,73 @@ mod block_based_test_suite_tests { n, min_value, } => ( - *step_count, - *decrease_per_interval_numerator, - *decrease_per_interval_denominator, - s.unwrap_or(1), - *n, - min_value.unwrap_or_default(), + *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 { - return n; + 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(|| { + println!("ERR: overflow in expected_emission({})", f_x); + 0 + }) + .max(min_value) } + } +} - // 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 as f64 - s as f64) as i32 / step_count as i32; // integer by purpose, we want to round down - let f_x = n as f64 * a.powi(b); +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_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}; + use simple_signer::signer::SimpleSigner; - // println!("expected_emission({}) = {}", x, f_x); - // f_x.to_u64().expect("expected to convert to u64") - f_x.to_u64().unwrap_or(min_value).max(min_value) + /// Run provided closure with timeout. + pub(super) 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(|e| format!("timeout after {:?}", e))? + .map_err(|e| format!("join error: {:?}", e))? } + /// Check that claim results at provided heights are as expected, and that balances match expectations. - fn check_heights( + /// + /// Note we take i128 into expected_balances, as we want to be able to detect overflows. + pub(super) fn check_heights( distribution_function: DistributionFunction, claim_heights: &[u64], expected_balances: &[u64], @@ -921,7 +996,7 @@ mod block_based_test_suite_tests { /// * `claim_heights` - heights at which claims will be made; they will see balance from previous height /// * `expected_balances` - expected balances after claims were made and block from `heights` was committed /// - fn check_heights_odd_no_current_rewards( + pub(super) fn check_heights_odd_no_current_rewards( distribution_function: DistributionFunction, claim_heights: &[u64], expected_balances: &[u64], @@ -982,17 +1057,6 @@ mod block_based_test_suite_tests { suite.execute(&tests) } -} - -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::prelude::{DataContract, IdentityPublicKey}; - use simple_signer::signer::SimpleSigner; pub(crate) struct TestSuite { platform: TempPlatform, From 4a42235fc7109dee795ae4bd3bb24fc111a995c9 Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Thu, 20 Mar 2025 16:46:42 +0100 Subject: [PATCH 04/28] chore: remove duplicate code --- .../distribution/perpetual/block_based.rs | 90 ------------------- 1 file changed, 90 deletions(-) diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/perpetual/block_based.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/perpetual/block_based.rs index 2cc16b5d1e7..bbd62d0cf67 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 @@ -591,96 +591,6 @@ mod block_based_perpetual_random { .expect("no rewards"); } } -mod matrix { - use dpp::{ - balances::credits::TokenAmount, - data_contract::associated_token::token_perpetual_distribution::distribution_function::DistributionFunction, - }; - use rust_decimal::prelude::ToPrimitive; - - use crate::execution::validation::state_transition::batch::tests::token::distribution::perpetual::block_based::{block_based_perpetual_step_decreasing::expected_emission, test_suite::check_heights}; - - // #[test_case::test_matrix( - // [1,10], // step_count - // [0,1,u16::MAX,999], // decrease_per_interval_numerator - // [0,1,2,10,100,u16::MAX], // decrease_per_interval_denominator - // [None,Some(1),Some(10),Some(u64::MAX)], // s - // [0,1,100,100_000, 1_000_000, 10_000_000, 100_000_000, u64::MAX], // n - // [None,Some(1),Some(10),Some(u64::MAX)], // min_value - // [0,110, 100, 1000] // distribution_interval - // )] - #[test_case::test_matrix( - [0,1,10], // step_count - [0,1,10,u16::MAX], // decrease_per_interval_numerator - [1], // decrease_per_interval_denominator - [None], // s - [0,1,100000], // n - [None], // min_value - [0,110, 100, 1000] // distribution_interval - )] - fn test_block_based_perpetual_step_decreasing_matrix( - step_count: u32, - decrease_per_interval_numerator: u16, - decrease_per_interval_denominator: u16, - s: Option, - n: TokenAmount, - min_value: Option, - distribution_interval: u64, - ) { - // - let dist = DistributionFunction::StepDecreasingAmount { - step_count, - decrease_per_interval_numerator, - decrease_per_interval_denominator, - s, - n, - min_value, - }; - - const VERY_HIGH_HEIGHT: u64 = 1_000_000; - let claim_heights = if distribution_interval > 0 { - let mut heights = (1..10) - .map(|i| i * distribution_interval) - .collect::>(); - heights.push(VERY_HIGH_HEIGHT); - - heights - } else { - vec![1, 2, 3, 10, 100, VERY_HIGH_HEIGHT] - }; - let expected_balances = claim_heights - .iter() - .map(|&h| { - // initial balance, defined in contract js - let mut expected_balance = 100_000; - // loop over blocks, starting with S, with step PERPETUAL_DISTRIBUTION_INTERVAL - if distribution_interval > 0 { - for i in (1..=h).step_by(distribution_interval as usize) { - expected_balance += expected_emission(i, &dist); - } - } - println!("expected balance at height {}: {}", h, expected_balance); - expected_balance.to_u64().unwrap_or(0) // to handle tests that overflow - }) - .collect::>(); - // we expect all tests to pass - let expect_pass = claim_heights.iter().map(|&_h| true).collect::>(); - - if let Err(e) = check_heights( - dist.clone(), - &claim_heights, - &expected_balances, - &expect_pass, - None, //Some(S), - distribution_interval, - ) { - // print dist to stderr - panic!("test failed for distribution function {:?}: {}", dist, e); - } else { - println!("test passed for distribution function {:?}", dist); - } - } -} mod block_based_perpetual_step_decreasing { use dpp::balances::credits::TokenAmount; From 27bad8da58ffcc6955123c4c2992cc6be769900b Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Thu, 20 Mar 2025 16:52:11 +0100 Subject: [PATCH 05/28] chore: self-review --- .../distribution/perpetual/block_based.rs | 31 ++++++++++++------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/perpetual/block_based.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/perpetual/block_based.rs index bbd62d0cf67..cb8966a4dbc 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 @@ -887,7 +887,7 @@ mod test_suite { }] }; - tests.push(TestCase { + tests.push(TestStep { name: format!("claim at height {}", height), base_height: *height - 1, base_time_ms: 10_200_000_000, @@ -956,7 +956,7 @@ mod test_suite { }] }; - tests.push(TestCase { + tests.push(TestStep { name: format!("claim at height {}", height), base_height: *height - 1, base_time_ms: 10_200_000_000, @@ -968,6 +968,7 @@ mod test_suite { suite.execute(&tests) } + /// Test engine to run tests for different token distribution functions. pub(crate) struct TestSuite { platform: TempPlatform, platform_version: &'static PlatformVersion, @@ -985,10 +986,11 @@ mod test_suite { } 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 { @@ -1020,6 +1022,7 @@ mod test_suite { } .with_genesis(1, genesis_time_ms) } + /// Lazily initialize and return token contract. Also sets token id. fn get_contract(&mut self) -> DataContract { if let Some(ref contract) = self.contract { @@ -1049,6 +1052,7 @@ mod test_suite { contract } + /// Get token ID or create if needed. fn get_token_id(&mut self) -> Identifier { if self.token_id.is_none() { @@ -1065,8 +1069,8 @@ mod test_suite { self.nonce } - // submit a claim transition and assert the results - pub(crate) fn assert_claim(&mut self, assertions: Vec) -> Result<(), String> { + /// 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 @@ -1133,6 +1137,7 @@ mod test_suite { Ok(()) } + /// Retrieve token balance for the identity and assert it matches expected value. pub(crate) fn assert_balance( &mut self, expected_balance: Option, @@ -1192,11 +1197,11 @@ mod test_suite { self.start_time = Some(start_time); self } - /// execute test cases - pub(super) fn execute(&mut self, tests: &[TestCase]) -> Result<(), String> { + /// 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_test_case(test_case); + let result = self.execute_step(test_case); if let Err(e) = result { errors += format!("\n--> {}: {}", test_case.name, e).as_str(); } @@ -1209,7 +1214,9 @@ mod test_suite { } } - pub(super) fn execute_test_case(&mut self, test_case: &TestCase) -> Result<(), String> { + /// 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; @@ -1229,7 +1236,7 @@ mod test_suite { self.epoch_index, false, ); - self.assert_claim(test_case.assertions.clone()) + self.claim(test_case.assertions.clone()) .map_err(|e| format!("claim failed: {}", e))?; self.assert_balance(Some(test_case.expected_balance)) .map_err(|e| format!("invalid balance: {}", e))?; @@ -1239,7 +1246,9 @@ mod test_suite { } pub(crate) type AssertionFn = fn(&[StateTransitionExecutionResult]) -> Result<(), String>; - pub(crate) struct TestCase { + + /// Individual step of a test case. + pub(crate) struct TestStep { pub(crate) name: String, /// height of block just before the claim pub(crate) base_height: u64, From 53576a189fe1c2122d2edf51292ae7110bb4b351 Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Fri, 21 Mar 2025 14:42:13 +0100 Subject: [PATCH 06/28] WIP --- .../distribution/perpetual/block_based.rs | 273 +++++++++++++++--- 1 file changed, 228 insertions(+), 45 deletions(-) diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/perpetual/block_based.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/perpetual/block_based.rs index cb8966a4dbc..5bd824362b3 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 @@ -547,9 +547,11 @@ mod block_based_perpetual_fixed_amount { fn test_block_based_perpetual_fixed_amount_0() { check_heights( DistributionFunction::FixedAmount { amount: 0 }, - &[41, 46, 50, 100000], - &[100000, 100000, 100000, 100000], - &[true, false, false, false], + &Claim::from_claims(( + &[41, 46, 50, 100000], + &[100000, 100000, 100000, 100000], + &[true, false, false, false], + )), None, 10, ) @@ -598,7 +600,7 @@ mod block_based_perpetual_step_decreasing { use rust_decimal::prelude::ToPrimitive; use test_case::test_case; use crate::execution::validation::state_transition::batch::tests::token::distribution::perpetual::block_based::test_suite::check_heights; - use super::test_suite::with_timeout; + use super::test_suite::{with_timeout, Claim}; const TIMEOUT: tokio::time::Duration = tokio::time::Duration::from_secs(1); @@ -735,9 +737,7 @@ mod block_based_perpetual_step_decreasing { with_timeout(TIMEOUT, move || { check_heights( dist, - &claim_heights, - &expected_balances, - &expect_pass, + &Claim::from_claims((&claim_heights, &expected_balances, &expect_pass)), None, //Some(S), distribution_interval, ) @@ -802,6 +802,144 @@ mod block_based_perpetual_step_decreasing { } } +mod block_based_perpetual_stepwise { + use std::collections::BTreeMap; + + use dpp::data_contract::associated_token::token_perpetual_distribution::distribution_function::DistributionFunction; + + use super::test_suite::{check_heights, with_timeout, Claim}; + + const TIMEOUT: tokio::time::Duration = tokio::time::Duration::from_secs(1); + + #[test] + fn stepwise_correct() { + let periods = BTreeMap::from([ + (0, 10_000), + (20, 20_000), + (45, 30_000), + (50, 40_000), + (70, 50_000), + ]); + + let dist = DistributionFunction::Stepwise(periods); + let distribution_interval = 10; + + // claims: height, balance, expect_pass + let claims = [ + (10, 110_000, true), + (11, 110_000, false), + (20, 120_000, true), + (24, 120_000, false), + (35, 140_000, true), + (39, 140_000, false), + (46, 160_000, true), + (49, 160_000, false), + (50, 180_000, true), + (51, 180_000, false), + (70, 270_000, true), + ( + 1_000_000, + 270_000 + 50_000 * (1_000_000 - 70_000) / distribution_interval, + true, + ), + ]; + + let claim_heights = claims.map(|x| x.0); + let expected_balances = claims.map(|x| x.1); + let expect_pass = claims.map(|x| x.2); + + with_timeout(TIMEOUT, move || { + check_heights( + dist, + &claim_heights, + &expected_balances, + &expect_pass, + None, //Some(S), + distribution_interval, + ) + }) + .inspect_err(|e| { + println!("{}", e); + }) + .expect("stepwise should pass"); + } + + // ===== HELPER FUNCTIONS ===== // + + #[test] + fn stepwise_u64_max() { + let periods = BTreeMap::from([(0, u64::MAX)]); + let dist = DistributionFunction::Stepwise(periods); + + check_heights( + dist, + &[100], + &[0], // doesn't matter, we expect overflow + &[false], + None, //Some(S), + 10, + ) + .inspect_err(|e| { + println!("{}", e); + }) + .expect("stepwise should pass"); + } + #[test] + /// We check what happens if we start distribution before the first period. + fn stepwise_before_first_period() { + let periods = BTreeMap::from([(100, 10_000)]); + let dist = DistributionFunction::Stepwise(periods); + + // claims: height, balance, expect_pass + let claims = [ + (1, 100_000, true), // IMO we should be able to claim first 100_000 here so expect_pass == true + (9, 100_000, false), // TODO: claim should succeed here? To transfer this 100k? + // (10, 0, false), + // (11, 0, false), + // (20, 0, false), + // (99, 0, false), + (100, 100_000, false), + (101, 110_000, true), + (102, 110_000, false), + (111, 120_000, true), + (200, 200_000, true), + (209, 200_000, false), + ]; + + check_heights( + dist, + &claims.map(|x| x.0), + &claims.map(|x| x.1), + &claims.map(|x| x.2), + None, + 10, + ) + .inspect_err(|e| { + println!("{}", e); + }) + .expect("stepwise should pass"); + } + + #[test] + /// This test will overflow within 6 distributions + fn stepwise_overflow() { + let periods = BTreeMap::from([(10, u64::MAX / 5)]); + let dist = DistributionFunction::Stepwise(periods); + + check_heights( + dist, + &[10, 11], + &[100_000], // doesn't matter, we expect overflow + &[false], + None, //Some(S), + 10, + ) + .inspect_err(|e| { + println!("{}", e); + }) + .expect("stepwise should pass"); + } +} mod test_suite { use super::*; use crate::rpc::core::MockCoreRPCLike; @@ -814,9 +952,10 @@ mod test_suite { 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}; + use dpp::prelude::{DataContract, IdentityPublicKey, TimestampMillis}; use simple_signer::signer::SimpleSigner; + const TIMEOUT: tokio::time::Duration = tokio::time::Duration::from_secs(1); /// Run provided closure with timeout. pub(super) fn with_timeout( duration: tokio::time::Duration, @@ -838,19 +977,17 @@ mod test_suite { /// 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. - pub(super) fn check_heights( + pub(super) fn check_heights + Clone>( distribution_function: DistributionFunction, - claim_heights: &[u64], - expected_balances: &[u64], - expect_pass: &[bool], - contract_start_height: Option, + claims: &[C], + contract_start_time: Option, distribution_interval: u64, ) -> Result<(), String> { let mut suite = TestSuite::new( 10_200_000_000, 0, TokenDistributionType::Perpetual, - Some(|token_configuration: &mut TokenConfiguration| { + Some(move |token_configuration: &mut TokenConfiguration| { token_configuration .distribution_rules_mut() .set_perpetual_distribution(Some(TokenPerpetualDistribution::V0( @@ -864,39 +1001,28 @@ mod test_suite { ))); }), ); - if let Some(start) = contract_start_height { + if let Some(start) = contract_start_time { suite = suite.with_contract_start_time(start); } let mut tests = Vec::new(); - for (i, height) in claim_heights.iter().enumerate() { - let assertions: Vec = if expect_pass[i] { - vec![|processing_results: &[_]| match processing_results { - [StateTransitionExecutionResult::SuccessfulExecution(_, _)] => Ok(()), - _ => Err(format!( - "expected SuccessfulExecution, got {:?}", - processing_results - )), - }] - } else { - vec![|processing_results: &[_]| match processing_results { - [StateTransitionExecutionResult::SuccessfulExecution(_, _)] => { - Err("expected error, got SuccessfulExecution".into()) - } - _ => Ok(()), - }] - }; + + let final_claims = claims + .iter() + .map(|item| item.clone().into()) + .collect::>(); + for item in claims { + let claim: Claim = item.clone().into(); tests.push(TestStep { - name: format!("claim at height {}", height), - base_height: *height - 1, + name: format!("claim at height {}", claim.claim_height), + base_height: claim.claim_height - 1, base_time_ms: 10_200_000_000, - expected_balance: expected_balances[i], - assertions, + expected_balance: claim.expected_balance, + claim_transition_assertions: claim.assertions(), }); } - - suite.execute(&tests) + with_timeout(TIMEOUT, move || suite.execute(&tests)) } /// This test checks claims at provided heights, where every second height does not have any rewards to claim. /// @@ -961,7 +1087,7 @@ mod test_suite { base_height: *height - 1, base_time_ms: 10_200_000_000, expected_balance: expected_balances[i], - assertions, + claim_transition_assertions: assertions, }); } @@ -977,7 +1103,7 @@ mod test_suite { identity_public_key: IdentityPublicKey, token_id: Option, contract: Option, - start_time: Option, + start_time: Option, token_distribution_type: TokenDistributionType, token_configuration_modification: Option, epoch_index: u16, @@ -1190,7 +1316,7 @@ mod test_suite { /// Configure custom contract start time; must be called before contract is /// initialized. - pub(super) fn with_contract_start_time(mut self, start_time: u64) -> Self { + 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"); } @@ -1203,7 +1329,7 @@ mod test_suite { for test_case in tests { let result = self.execute_step(test_case); if let Err(e) = result { - errors += format!("\n--> {}: {}", test_case.name, e).as_str(); + errors += format!("\n--> {}: {}\n", test_case.name, e).as_str(); } } @@ -1236,7 +1362,7 @@ mod test_suite { self.epoch_index, false, ); - self.claim(test_case.assertions.clone()) + self.claim(test_case.claim_transition_assertions.clone()) .map_err(|e| format!("claim failed: {}", e))?; self.assert_balance(Some(test_case.expected_balance)) .map_err(|e| format!("invalid balance: {}", e))?; @@ -1257,7 +1383,64 @@ mod test_suite { /// expected balance is a function that should return the expected balance after committing block /// at provided height and time pub(crate) expected_balance: u64, - /// assertion functions that will be executed on the claim - pub(crate) assertions: Vec, + /// assertion functions that must be met after executing the claim state transition + pub(crate) claim_transition_assertions: Vec, + } + + impl TestStep { + pub(super) fn new( + claim_height: u64, + expected_balance: u64, + expect_claim_successful: bool, + ) -> Self { + let assertions: Vec = if expect_claim_successful { + vec![|processing_results: &[_]| match processing_results { + [StateTransitionExecutionResult::SuccessfulExecution(_, _)] => Ok(()), + _ => Err(format!( + "expected SuccessfulExecution, got {:?}", + processing_results + )), + }] + } else { + vec![|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(()), + }] + }; + Self { + name: format!("claim at height {}", claim_height), + base_height: claim_height - 1, + base_time_ms: 10_200_000_000, + expected_balance, + claim_transition_assertions: assertions, + } + } + + // just a helper to faster update existing code + pub(super) fn from_claims( + (claim_heights, expected_balances, expect_pass): (&[u64], &[u64], &[bool]), + ) -> Vec { + assert_eq!(claim_heights.len(), expected_balances.len()); + assert_eq!(claim_heights.len(), expect_pass.len()); + claim_heights + .iter() + .zip(expected_balances.iter()) + .zip(expect_pass.iter()) + .map(|((&h, &balance), &expect)| Claim::new(h, balance, expect)) + .collect() + } + } + + 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 91de94c86c772e46a5a6b21c13886de09a24bb3a Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Mon, 24 Mar 2025 18:35:42 +0100 Subject: [PATCH 07/28] chore: check entropy of random distribution --- .../distribution/perpetual/block_based.rs | 489 +++++++++++------- 1 file changed, 290 insertions(+), 199 deletions(-) diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/perpetual/block_based.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/perpetual/block_based.rs index 5bd824362b3..3aa327651a3 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 @@ -505,12 +505,16 @@ mod block_based_perpetual_fixed_amount { // When a claim is made at block 42, // Then the claim should be successful. #[test] - fn test_block_based_perpetual_fixed_amount_50() { - super::test_suite::check_heights_odd_no_current_rewards( + super::test_suite::check_heights( DistributionFunction::FixedAmount { amount: 50 }, - &[41, 46, 50, 1000], - &[100200, 100200, 100250], + &[ + 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, ) .expect("\n-> fixed amount should pass"); @@ -522,76 +526,222 @@ mod block_based_perpetual_fixed_amount { /// got [InternalError(\"storage: protocol: overflow error: Overflow in FixedAmount evaluation\")]" #[test] fn test_block_based_perpetual_fixed_amount_1_000_000_000() { - check_heights_odd_no_current_rewards( + check_heights( DistributionFunction::FixedAmount { amount: 1_000_000_000, }, - &[41, 46, 50, 51, 1_000_000_000_000], &[ - 100_000 + 4 * 1_000_000_000, - 100_000 + 4 * 1_000_000_000, - 100_000 + 5 * 1_000_000_000, - 100_000 + 5 * 1_000_000_000, - 1, // 100_000 + (1_000_000_000_000 / 10) * 1_000_000_000, -- this will overflow + TestStep::new(41, 100_000 + 4 * 1_000_000_000, true), + TestStep::new(46, 100_000 + 4 * 1_000_000_000, false), + TestStep::new(50, 100_000 + 5 * 1_000_000_000, true), + TestStep::new(51, 100_000 + 5 * 1_000_000_000, false), + TestStep::new(1_000_000_000_000, 100_000 + 5 * 1_000_000_000, false), ], + None, 10, ) .expect("\n-> fixed amount should pass"); } #[test] - /// With a fixed amount of 0, we expect first claim to fetch 100_000 units (which are in the contract defintion), + /// With a fixed amount of 0, we expect first claim to fetch 100_000 units (which are hardcoded in the JSON contract defintion), /// and fail for the rest of the claims. /// /// FAILS fn test_block_based_perpetual_fixed_amount_0() { check_heights( DistributionFunction::FixedAmount { amount: 0 }, - &Claim::from_claims(( - &[41, 46, 50, 100000], - &[100000, 100000, 100000, 100000], - &[true, false, false, false], - )), + &[ + (41, 100000, true), + (46, 100000, false), + (50, 100000, false), + (1000, 100000, false), + ], None, 10, ) .expect("\nfixed amount zero increase\n"); } + /// Overflow caused by using u64::MAX as fixed amount should not cause InternalError. #[test] fn test_block_based_perpetual_fixed_amount_u64_max() { - check_heights_odd_no_current_rewards( + check_heights( DistributionFunction::FixedAmount { amount: u64::MAX }, - &[41, 46, 50, 1000], - &[100200, 100200, 100250, 100250], + &[ + TestStep::new(41, 100_200, true), + TestStep::new(46, 100_200, false), + TestStep::new(50, 100_250, true), + TestStep::new(1000, 100_250, false), + ], + None, 10, ) .expect("\nfixed amount u64::MAX should pass\n"); } } mod block_based_perpetual_random { - use dpp::data_contract::associated_token::token_perpetual_distribution::distribution_function::DistributionFunction; - - use super::test_suite::check_heights_odd_no_current_rewards; + 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}; + 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, + }; + /// Random distribution function with min=0, max=100. #[test] - fn test_block_based_perpetual_random() { - check_heights_odd_no_current_rewards( + fn test_block_based_perpetual_random_0_100() { + check_heights( DistributionFunction::Random { min: 0, max: 100 }, - &[41, 46, 50, 59, 60], - &[100192, 100192, 100263, 100263, 100310], + &[ + 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), + ], + None, 10, ) .expect("correct case 1"); + } - check_heights_odd_no_current_rewards( + /// Random distribution function with min=0, max=0 should only return the initial balance. + #[test] + fn test_block_based_perpetual_random_0_0() { + check_heights( DistributionFunction::Random { min: 0, max: 0 }, - &[41], - &[100192], + &[ + TestStep::new(41, 100_000, true), + TestStep::new(50, 100_000, false), + TestStep::new(100, 100_000, false), + ], + None, 10, ) .expect("no rewards"); } + + /// Check if the random function is truly random by estimating its 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(); + + // we expect the average to be 200; we add 100_000 which is the initial balance + // let expected_balance = ((((MIN + MAX) as f64) / 2.0) * (N as f64)) as u64 + 100_000; + // tests.push(TestStep { + // name: "last test".to_string(), + // base_height: N - 1, + // base_time_ms: Default::default(), + // expected_balance: Some(expected_balance), + // claim_transition_assertions: Default::default(), + // }); + + 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(100_000, |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; + + println!("Data: {:?}", diffs); + println!( + "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 block_based_perpetual_step_decreasing { @@ -600,9 +750,6 @@ mod block_based_perpetual_step_decreasing { use rust_decimal::prelude::ToPrimitive; use test_case::test_case; use crate::execution::validation::state_transition::batch::tests::token::distribution::perpetual::block_based::test_suite::check_heights; - use super::test_suite::{with_timeout, Claim}; - - const TIMEOUT: tokio::time::Duration = tokio::time::Duration::from_secs(1); #[test_case( 1,// step_count @@ -734,14 +881,19 @@ mod block_based_perpetual_step_decreasing { // we expect all tests to pass let expect_pass = claim_heights.iter().map(|&_h| true).collect::>(); - with_timeout(TIMEOUT, move || { - check_heights( - dist, - &Claim::from_claims((&claim_heights, &expected_balances, &expect_pass)), - None, //Some(S), - distribution_interval, - ) - }) + let claims = claim_heights + .iter() + .zip(expected_balances.iter()) + .zip(expect_pass.iter()) + .map(|((&h, &b), &p)| (h, b, p)) + .collect::>(); + + check_heights( + dist, + &claims, + None, //Some(S), + distribution_interval, + ) .inspect_err(|e| { println!("{}", e); }) @@ -803,13 +955,9 @@ mod block_based_perpetual_step_decreasing { } mod block_based_perpetual_stepwise { - use std::collections::BTreeMap; - + use super::test_suite::check_heights; use dpp::data_contract::associated_token::token_perpetual_distribution::distribution_function::DistributionFunction; - - use super::test_suite::{check_heights, with_timeout, Claim}; - - const TIMEOUT: tokio::time::Duration = tokio::time::Duration::from_secs(1); + use std::collections::BTreeMap; #[test] fn stepwise_correct() { @@ -844,20 +992,12 @@ mod block_based_perpetual_stepwise { ), ]; - let claim_heights = claims.map(|x| x.0); - let expected_balances = claims.map(|x| x.1); - let expect_pass = claims.map(|x| x.2); - - with_timeout(TIMEOUT, move || { - check_heights( - dist, - &claim_heights, - &expected_balances, - &expect_pass, - None, //Some(S), - distribution_interval, - ) - }) + check_heights( + dist, + &claims, + None, //Some(S), + distribution_interval, + ) .inspect_err(|e| { println!("{}", e); }) @@ -873,9 +1013,7 @@ mod block_based_perpetual_stepwise { check_heights( dist, - &[100], - &[0], // doesn't matter, we expect overflow - &[false], + &[(100, 0, false)], None, //Some(S), 10, ) @@ -906,18 +1044,11 @@ mod block_based_perpetual_stepwise { (209, 200_000, false), ]; - check_heights( - dist, - &claims.map(|x| x.0), - &claims.map(|x| x.1), - &claims.map(|x| x.2), - None, - 10, - ) - .inspect_err(|e| { - println!("{}", e); - }) - .expect("stepwise should pass"); + check_heights(dist, &claims, None, 10) + .inspect_err(|e| { + println!("{}", e); + }) + .expect("stepwise should pass"); } #[test] @@ -928,9 +1059,7 @@ mod block_based_perpetual_stepwise { check_heights( dist, - &[10, 11], - &[100_000], // doesn't matter, we expect overflow - &[false], + &[(10, 100_000, false), (11, 100_000, false)], None, //Some(S), 10, ) @@ -955,9 +1084,10 @@ mod test_suite { use dpp::prelude::{DataContract, IdentityPublicKey, TimestampMillis}; use simple_signer::signer::SimpleSigner; - const TIMEOUT: tokio::time::Duration = tokio::time::Duration::from_secs(1); + const TIMEOUT: tokio::time::Duration = tokio::time::Duration::from_secs(10); /// Run provided closure with timeout. - pub(super) fn 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> { @@ -970,16 +1100,29 @@ mod test_suite { let worker = rt.spawn_blocking(f); rt.block_on(async move { tokio::time::timeout(duration, worker).await }) - .map_err(|e| format!("timeout after {:?}", e))? + .map_err(|e| 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. - pub(super) fn check_heights + Clone>( + /// + /// # 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 + /// + /// 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, - claims: &[C], + steps: &[C], contract_start_time: Option, distribution_interval: u64, ) -> Result<(), String> { @@ -1005,93 +1148,12 @@ mod test_suite { suite = suite.with_contract_start_time(start); } - let mut tests = Vec::new(); - - let final_claims = claims + let steps = steps .iter() .map(|item| item.clone().into()) - .collect::>(); - for item in claims { - let claim: Claim = item.clone().into(); + .collect::>(); - tests.push(TestStep { - name: format!("claim at height {}", claim.claim_height), - base_height: claim.claim_height - 1, - base_time_ms: 10_200_000_000, - expected_balance: claim.expected_balance, - claim_transition_assertions: claim.assertions(), - }); - } - with_timeout(TIMEOUT, move || suite.execute(&tests)) - } - /// This test checks claims at provided heights, where every second height does not have any rewards to claim. - /// - /// # Arguments - /// - /// * `distribution_function` - configured distribution function to test - /// * `claim_heights` - heights at which claims will be made; they will see balance from previous height - /// * `expected_balances` - expected balances after claims were made and block from `heights` was committed - /// - pub(super) fn check_heights_odd_no_current_rewards( - distribution_function: DistributionFunction, - claim_heights: &[u64], - expected_balances: &[u64], - distribution_interval: u64, - ) -> Result<(), String> { - let mut suite = TestSuite::new( - 10_200_000_000, - 0, - TokenDistributionType::Perpetual, - Some(|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, - }, - ))); - }), - ); - - let mut tests = Vec::new(); - for (i, height) in claim_heights.iter().enumerate() { - let assertions: Vec = if i % 2 == 0 { - vec![|processing_results: &[_]| match processing_results { - [StateTransitionExecutionResult::SuccessfulExecution(_, _)] => Ok(()), - _ => Err(format!( - "expected SuccessfulExecution, got {:?}", - processing_results - )), - }] - } else { - vec![|processing_results: &[_]| match processing_results { - [StateTransitionExecutionResult::PaidConsensusError( - ConsensusError::StateError(StateError::InvalidTokenClaimNoCurrentRewards( - _, - )), - _, - )] => Ok(()), - _ => Err(format!( - "expected InvalidTokenClaimNoCurrentRewards, got {:?}", - processing_results - )), - }] - }; - - tests.push(TestStep { - name: format!("claim at height {}", height), - base_height: *height - 1, - base_time_ms: 10_200_000_000, - expected_balance: expected_balances[i], - claim_transition_assertions: assertions, - }); - } - - suite.execute(&tests) + with_timeout(TIMEOUT, move || suite.execute(&steps)) } /// Test engine to run tests for different token distribution functions. @@ -1109,6 +1171,13 @@ mod test_suite { 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 { @@ -1145,6 +1214,7 @@ mod test_suite { nonce: 1, time_between_blocks, token_configuration_modification, + on_step_success: Box::new(|_| {}), } .with_genesis(1, genesis_time_ms) } @@ -1264,13 +1334,10 @@ mod test_suite { } /// Retrieve token balance for the identity and assert it matches expected value. - pub(crate) fn assert_balance( - &mut self, - expected_balance: Option, - ) -> Result<(), String> { + pub(crate) fn get_balance(&mut self) -> Result, String> { let token_id = self.get_token_id().to_buffer(); - let token_balance = self - .platform + + self.platform .drive .fetch_identity_token_balance( token_id, @@ -1278,7 +1345,15 @@ mod test_suite { None, self.platform_version, ) - .expect("expected to fetch token balance"); + .map_err(|e| format!("failed to fetch token balance: {}", e)) + } + + /// 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!( @@ -1323,6 +1398,21 @@ mod test_suite { 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(); @@ -1364,8 +1454,21 @@ mod test_suite { ); self.claim(test_case.claim_transition_assertions.clone()) .map_err(|e| format!("claim failed: {}", e))?; - self.assert_balance(Some(test_case.expected_balance)) - .map_err(|e| format!("invalid balance: {}", 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 let Some(expected_balance) = test_case.expected_balance { + if expected_balance != balance { + return Err(format!( + "expected balance {:?} but got {:?}", + test_case.expected_balance, balance + )); + } + }; + (self.on_step_success)(balance); Ok(()) } @@ -1374,6 +1477,7 @@ mod test_suite { 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 @@ -1382,18 +1486,19 @@ mod test_suite { 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: u64, + 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 { - pub(super) fn new( - claim_height: u64, - expected_balance: u64, - expect_claim_successful: bool, - ) -> Self { - let assertions: Vec = if expect_claim_successful { + /// 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 assertions: Vec = if expect_success { vec![|processing_results: &[_]| match processing_results { [StateTransitionExecutionResult::SuccessfulExecution(_, _)] => Ok(()), _ => Err(format!( @@ -1416,24 +1521,10 @@ mod test_suite { name: format!("claim at height {}", claim_height), base_height: claim_height - 1, base_time_ms: 10_200_000_000, - expected_balance, + expected_balance: Some(expected_balance), claim_transition_assertions: assertions, } } - - // just a helper to faster update existing code - pub(super) fn from_claims( - (claim_heights, expected_balances, expect_pass): (&[u64], &[u64], &[bool]), - ) -> Vec { - assert_eq!(claim_heights.len(), expected_balances.len()); - assert_eq!(claim_heights.len(), expect_pass.len()); - claim_heights - .iter() - .zip(expected_balances.iter()) - .zip(expect_pass.iter()) - .map(|((&h, &balance), &expect)| Claim::new(h, balance, expect)) - .collect() - } } impl From<(u64, u64, bool)> for TestStep { From 3dc1c9b10d508a0f0085b52e24ea9f229b993164 Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Tue, 25 Mar 2025 14:29:01 +0100 Subject: [PATCH 08/28] wip --- .../distribution/perpetual/block_based.rs | 467 ++++++++++++------ 1 file changed, 320 insertions(+), 147 deletions(-) diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/perpetual/block_based.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/perpetual/block_based.rs index 3aa327651a3..da6f8e62a5f 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; @@ -498,8 +502,13 @@ mod perpetual_distribution_block { #[cfg(test)] mod block_based_perpetual_fixed_amount { - use super::test_suite::*; - use dpp::data_contract::associated_token::token_perpetual_distribution::distribution_function::DistributionFunction; + 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, + }; // Given some token configuration, // When a claim is made at block 42, @@ -516,66 +525,100 @@ mod block_based_perpetual_fixed_amount { ], 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 test_block_based_perpetual_fixed_amount_1_000_000_000() { + fn fail_test_block_based_perpetual_fixed_amount_1_000_000_000() { check_heights( DistributionFunction::FixedAmount { amount: 1_000_000_000, }, &[ - TestStep::new(41, 100_000 + 4 * 1_000_000_000, true), - TestStep::new(46, 100_000 + 4 * 1_000_000_000, false), - TestStep::new(50, 100_000 + 5 * 1_000_000_000, true), - TestStep::new(51, 100_000 + 5 * 1_000_000_000, false), - TestStep::new(1_000_000_000_000, 100_000 + 5 * 1_000_000_000, false), + 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] - /// With a fixed amount of 0, we expect first claim to fetch 100_000 units (which are hardcoded in the JSON contract defintion), - /// and fail for the rest of the claims. - /// - /// FAILS + /// Given a fixed amount distribution with value of 0, + /// When we try to claim, + /// Then we always fail and the balance remains unchanged. fn test_block_based_perpetual_fixed_amount_0() { check_heights( DistributionFunction::FixedAmount { amount: 0 }, &[ - (41, 100000, true), + (41, 100000, false), (46, 100000, false), (50, 100000, false), (1000, 100000, false), ], None, 10, + None, ) .expect("\nfixed amount zero increase\n"); } - /// Overflow caused by using u64::MAX as fixed amount should not cause InternalError. #[test] - fn test_block_based_perpetual_fixed_amount_u64_max() { + /// 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 test_fixed_amount_above_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_200, true), - TestStep::new(46, 100_200, false), - TestStep::new(50, 100_250, true), - TestStep::new(1000, 100_250, false), - ], + &[TestStep::new(41, 100_000, false)], None, 10, + None, ) .expect("\nfixed amount u64::MAX should pass\n"); } @@ -588,7 +631,10 @@ mod block_based_perpetual_random { use crate::execution::validation::state_transition::batch::tests::token::distribution::perpetual::block_based::test_suite::TestSuite; - use super::test_suite::{check_heights, TestStep}; + use super::{ + test_suite::{check_heights, TestStep}, + INITIAL_BALANCE, + }; use dpp::data_contract::{ associated_token::{ token_configuration::accessors::v0::TokenConfigurationV0Getters, @@ -603,42 +649,56 @@ mod block_based_perpetual_random { }, TokenConfiguration, }; + use test_case::test_matrix; - /// Random distribution function with min=0, max=100. - #[test] - fn test_block_based_perpetual_random_0_100() { + /// , steps: &[TestStep]) { check_heights( - DistributionFunction::Random { min: 0, max: 100 }, - &[ - 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), - ], + DistributionFunction::Random { min, max }, + steps, None, 10, + Some(max_supply), ) .expect("correct case 1"); } - /// Random distribution function with min=0, max=0 should only return the initial balance. + /// 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, 100_000, true), - TestStep::new(50, 100_000, false), - TestStep::new(100, 100_000, false), + TestStep::new(41, INITIAL_BALANCE, false), + TestStep::new(50, INITIAL_BALANCE, false), + TestStep::new(100, INITIAL_BALANCE, false), ], None, 10, + None, ) .expect("no rewards"); } - /// Check if the random function is truly random by estimating its entropy. + /// 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; @@ -655,16 +715,6 @@ mod block_based_perpetual_random { }) .collect(); - // we expect the average to be 200; we add 100_000 which is the initial balance - // let expected_balance = ((((MIN + MAX) as f64) / 2.0) * (N as f64)) as u64 + 100_000; - // tests.push(TestStep { - // name: "last test".to_string(), - // base_height: N - 1, - // base_time_ms: Default::default(), - // expected_balance: Some(expected_balance), - // claim_transition_assertions: Default::default(), - // }); - let balances = Arc::new(Mutex::new(Vec::new())); let balances_result = balances.clone(); @@ -696,7 +746,7 @@ mod block_based_perpetual_random { // substract balance from previous step (for first step, substract initial balance of 100_000) let diffs: Vec = data .iter() - .scan(100_000, |prev, &x| { + .scan(INITIAL_BALANCE, |prev, &x| { let diff = x - *prev; *prev = x; Some(diff) @@ -707,8 +757,8 @@ mod block_based_perpetual_random { let max_entropy: f64 = ((MAX - MIN) as f64).log2(); let entropy_diff = (max_entropy - entropy).abs() / max_entropy; - println!("Data: {:?}", diffs); - println!( + tracing::debug!("Data: {:?}", diffs); + tracing::info!( "Entropy: {}, max entropy: {}, difference: {}%", entropy, max_entropy, @@ -750,6 +800,7 @@ mod block_based_perpetual_step_decreasing { use rust_decimal::prelude::ToPrimitive; use test_case::test_case; 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; #[test_case( 1,// step_count @@ -771,7 +822,7 @@ mod block_based_perpetual_step_decreasing { Some(1),// min_value Some((1..1000).step_by(500).collect()),// claim_heights 1; // distribution_interval - "claim every 500 blocks" + "fail claim every 500 blocks" )] #[test_matrix( 1,// step_count @@ -793,7 +844,7 @@ mod block_based_perpetual_step_decreasing { Some(1),// min_value Some(vec![1,7]), // claim_heights 1; // distribution_interval - "1000x increase, overflow" + "fail 1000x increase, overflow" )] #[test_case( 1,// step_count @@ -817,6 +868,17 @@ mod block_based_perpetual_step_decreasing { 1; // distribution_interval "no decrease, irrelevant min values" )] + #[test_matrix( + [1],// step_count + 1,// decrease_per_interval_numerator + 1,// decrease_per_interval_denominator + None,// s + 100_000,// n + None,// min_value + Some(vec![7,10,20,100]), // claim_heights // ,300,500,800,1_000,1_000_000 + [5]; // distribution_interval + "no decrease, changing step" + )] #[test_matrix( [5,10],// step_count 1,// decrease_per_interval_numerator @@ -824,7 +886,7 @@ mod block_based_perpetual_step_decreasing { None,// s 100_000,// n None,// min_value - Some(vec![5,10,100]), // claim_heights // ,300,500,800,1_000,1_000_000 + Some(vec![5,10,20,100]), // claim_heights // ,300,500,800,1_000,1_000_000 [1,5]; // distribution_interval "1/2 decrease, changing step" )] @@ -866,14 +928,14 @@ mod block_based_perpetual_step_decreasing { .iter() .map(|&h| { // initial balance, defined in contract js - let mut expected_balance: i128 = 100_000; + 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); } - println!("expected balance at height {}: {}", h, expected_balance); + tracing::debug!("expected balance at height {}: {}", h, expected_balance); expected_balance.to_u64().unwrap_or_else(|| { - println!("ERR: overflow in expected balance at height {}", h); + tracing::error!("overflow in expected balance at height {}", h); 0 }) // to handle tests that overflow }) @@ -893,9 +955,10 @@ mod block_based_perpetual_step_decreasing { &claims, None, //Some(S), distribution_interval, + None, ) .inspect_err(|e| { - println!("{}", e); + tracing::error!("{}", e); }) } @@ -946,7 +1009,7 @@ mod block_based_perpetual_step_decreasing { let f_x = n as f64 * a.powi(b.to_i32().expect("overflow")); f_x.to_i128() .unwrap_or_else(|| { - println!("ERR: overflow in expected_emission({})", f_x); + tracing::error!("overflow in expected_emission({})", f_x); 0 }) .max(min_value) @@ -973,7 +1036,7 @@ mod block_based_perpetual_stepwise { let distribution_interval = 10; // claims: height, balance, expect_pass - let claims = [ + let steps = [ (10, 110_000, true), (11, 110_000, false), (20, 120_000, true), @@ -994,81 +1057,103 @@ mod block_based_perpetual_stepwise { check_heights( dist, - &claims, + &steps, None, //Some(S), distribution_interval, + None, ) .inspect_err(|e| { - println!("{}", e); + tracing::error!("{}", e); }) .expect("stepwise should pass"); } +} - // ===== HELPER FUNCTIONS ===== // +mod block_based_perpetual_linear { + use super::{test_suite::check_heights, INITIAL_BALANCE}; + use dpp::data_contract::associated_token::token_perpetual_distribution::distribution_function::DistributionFunction; + use rust_decimal::prelude::ToPrimitive; + use test_case::test_case; - #[test] - fn stepwise_u64_max() { - let periods = BTreeMap::from([(0, u64::MAX)]); - let dist = DistributionFunction::Stepwise(periods); + #[test_case(DistributionFunction::Linear{ + a: 1, + d: 1, + start_step: None, + starting_amount: 100_000, + min_value: None, + max_value: None + }, + &[10 ], + 1 + ; "x=y")] + + fn test_linear(dist: DistributionFunction, heights: &[u64], distribution_interval: u64) { + // Linear distribution function + // + // # Formula + // The formula for the linear distribution function is: + + // ```text + // f(x) = (a * (x - start_moment) / d) + starting_amount + // ``` + // + let steps = heights + .iter() + .scan((INITIAL_BALANCE, 1), |(balance, last_height), &h| { + for i in (*last_height..=h).step_by(distribution_interval as usize) { + *balance += expected_emission(i, 1, 1, None, 100_000); + } + *last_height = h; + Some((h, *balance, true)) + }) + .collect::>(); check_heights( dist, - &[(100, 0, false)], + &steps, None, //Some(S), - 10, + distribution_interval, + None, ) .inspect_err(|e| { - println!("{}", e); + tracing::error!("{}", e); }) .expect("stepwise should pass"); } - #[test] - /// We check what happens if we start distribution before the first period. - fn stepwise_before_first_period() { - let periods = BTreeMap::from([(100, 10_000)]); - let dist = DistributionFunction::Stepwise(periods); - - // claims: height, balance, expect_pass - let claims = [ - (1, 100_000, true), // IMO we should be able to claim first 100_000 here so expect_pass == true - (9, 100_000, false), // TODO: claim should succeed here? To transfer this 100k? - // (10, 0, false), - // (11, 0, false), - // (20, 0, false), - // (99, 0, false), - (100, 100_000, false), - (101, 110_000, true), - (102, 110_000, false), - (111, 120_000, true), - (200, 200_000, true), - (209, 200_000, false), - ]; - check_heights(dist, &claims, None, 10) - .inspect_err(|e| { - println!("{}", e); - }) - .expect("stepwise should pass"); - } + /// Calculate expected emission at provided height. + /// + /// All calculations are done in i128 to better handle overflows. + /// + /// ```text + /// f(x) = (a * (x - start_moment) / d) + starting_amount + /// ``` + fn expected_emission( + x: u64, + a: u64, + d: u64, + start_moment: Option, + starting_amount: u64, + ) -> u64 { + let x = x as i128; + let a = a as i128; + let d = d as i128; + let start_moment = start_moment.unwrap_or(0) as i128; + let starting_amount = starting_amount as i128; - #[test] - /// This test will overflow within 6 distributions - fn stepwise_overflow() { - let periods = BTreeMap::from([(10, u64::MAX / 5)]); - let dist = DistributionFunction::Stepwise(periods); + let f_x = if x < start_moment { + starting_amount + } else { + (a * (x - start_moment) / d + starting_amount).max(0) + }; - check_heights( - dist, - &[(10, 100_000, false), (11, 100_000, false)], - None, //Some(S), - 10, - ) - .inspect_err(|e| { - println!("{}", e); + f_x.to_u64().unwrap_or_else(|| { + tracing::error!("overflow in expected_emission({}), using 0", f_x); + 0 }) - .expect("stepwise should pass"); } } + mod test_suite { use super::*; use crate::rpc::core::MockCoreRPCLike; @@ -1100,7 +1185,7 @@ mod test_suite { let worker = rt.spawn_blocking(f); rt.block_on(async move { tokio::time::timeout(duration, worker).await }) - .map_err(|e| format!("test timed out after {:?}", TIMEOUT))? + .map_err(|_| format!("test timed out after {:?}", TIMEOUT))? .map_err(|e| format!("join error: {:?}", e))? } @@ -1114,6 +1199,7 @@ mod test_suite { /// * `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 @@ -1125,6 +1211,7 @@ mod test_suite { steps: &[C], contract_start_time: Option, distribution_interval: u64, + max_supply: Option>, ) -> Result<(), String> { let mut suite = TestSuite::new( 10_200_000_000, @@ -1144,6 +1231,10 @@ mod test_suite { ))); }), ); + if let Some(max_supply) = max_supply { + suite = suite.with_max_suppy(max_supply); + } + if let Some(start) = contract_start_time { suite = suite.with_contract_start_time(start); } @@ -1156,8 +1247,9 @@ mod test_suite { 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 { + pub(crate) struct TestSuite { platform: TempPlatform, platform_version: &'static PlatformVersion, identity: dpp::prelude::Identity, @@ -1167,7 +1259,7 @@ mod test_suite { contract: Option, start_time: Option, token_distribution_type: TokenDistributionType, - token_configuration_modification: Option, + token_configuration_modification: Option>, epoch_index: u16, nonce: u64, time_between_blocks: u64, @@ -1180,10 +1272,10 @@ mod test_suite { on_step_success: Box, } - impl TestSuite { + impl TestSuite { /// Create new test suite that will start at provided genesis time and create token contract with provided /// configuration. - pub(crate) fn new( + pub(crate) fn new( genesis_time_ms: u64, time_between_blocks: u64, token_distribution_type: TokenDistributionType, @@ -1195,12 +1287,14 @@ mod test_suite { .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)); - Self { + let me = Self { platform, platform_version, identity, @@ -1213,10 +1307,61 @@ mod test_suite { epoch_index: 1, nonce: 1, time_between_blocks, - token_configuration_modification, + token_configuration_modification: None, // setup later on_step_success: Box::new(|_| {}), } - .with_genesis(1, genesis_time_ms) + .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_suppy(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. @@ -1452,25 +1597,32 @@ mod test_suite { self.epoch_index, false, ); - self.claim(test_case.claim_transition_assertions.clone()) - .map_err(|e| format!("claim failed: {}", e))?; + 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 let Some(expected_balance) = test_case.expected_balance { - if expected_balance != balance { - return Err(format!( - "expected balance {:?} but got {:?}", - test_case.expected_balance, balance - )); - } - }; - (self.on_step_success)(balance); + 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 + )); + } - Ok(()) + if result.is_empty() { + (self.on_step_success)(balance); + Ok(()) + } else { + Err(result.join("\n")) + } } } @@ -1498,24 +1650,45 @@ mod test_suite { /// /// 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: &[_]| match processing_results { - [StateTransitionExecutionResult::SuccessfulExecution(_, _)] => Ok(()), - _ => Err(format!( - "expected SuccessfulExecution, got {:?}", - processing_results - )), - }] + 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: &[_]| match processing_results { - [StateTransitionExecutionResult::SuccessfulExecution(_, _)] => { - Err("expected error, got SuccessfulExecution".into()) - } - [StateTransitionExecutionResult::InternalError(e)] => { - Err(format!("expected normal error, got InternalError: {}", e)) - } - _ => Ok(()), - }] + 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), From a9cbf97ed7b8df08c5f5e97e524d8a6b0281bbd5 Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Wed, 26 Mar 2025 16:33:28 +0100 Subject: [PATCH 09/28] chore: wip --- .../distribution/perpetual/block_based.rs | 309 ++++++++++++------ 1 file changed, 217 insertions(+), 92 deletions(-) diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/perpetual/block_based.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/perpetual/block_based.rs index da6f8e62a5f..6e60d3b6e34 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 @@ -656,8 +656,8 @@ mod block_based_perpetual_random { /// Then I get deterministic balances at those heights. #[test_matrix( 0, //min - 100,//max, - [None,Some(1_000_000)], // max_supply + 100,//max, + [None,Some(1_000_000)], // max_supply &[ TestStep::new(41, 100_192, true), TestStep::new(46, 100_192, false), @@ -695,6 +695,24 @@ mod block_based_perpetual_random { ) .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, @@ -822,18 +840,29 @@ mod block_based_perpetual_step_decreasing { Some(1),// min_value Some((1..1000).step_by(500).collect()),// claim_heights 1; // distribution_interval - "fail claim every 500 blocks" + "fails: claim every 500 blocks" )] - #[test_matrix( + #[test_case( + 1,// step_count + 101,// decrease_per_interval_numerator + 100,// decrease_per_interval_denominator + None,// s + 100_000,// n + Some(1),// min_value + Some((1..1000).step_by(100).collect()),// claim_heights + 1; // distribution_interval + "1% increase, claim every 100 blocks" + )] + #[test_case( 1,// step_count 101,// decrease_per_interval_numerator 100,// decrease_per_interval_denominator None,// s 100_000,// n Some(1),// min_value - [Some((1..1000).step_by(100).collect()),Some((1..1000).step_by(500).collect())],// claim_heights + Some((1..1000).step_by(500).collect()),// claim_heights 1; // distribution_interval - "1% increase, varying claim heights" + "fails: 1% increase, claim every 500 blocks" )] #[test_case( 1,// step_count @@ -844,19 +873,30 @@ mod block_based_perpetual_step_decreasing { Some(1),// min_value Some(vec![1,7]), // claim_heights 1; // distribution_interval - "fail 1000x increase, overflow" + "fails: 1000x increase, overflow" )] - #[test_case( + #[test_matrix( 1,// step_count 1,// decrease_per_interval_numerator 1,// decrease_per_interval_denominator None,// s 100_000,// n - Some(1),// min_value + [Some(1), Some(100)],// min_value Some(vec![1,2,3,10,100]), // claim_heights // ,300,500,800,1_000,1_000_000 1; // distribution_interval "100% decrease, various min values" )] + #[test_case( + 1,// step_count + 1,// decrease_per_interval_numerator + 1,// decrease_per_interval_denominator + None,// s + 100_000,// n + Some(u64::MAX),// min_value + Some(vec![1,2,3,10,100]), // claim_heights // ,300,500,800,1_000,1_000_000 + 1; // distribution_interval + "fails: full decrease, min is u64::MAX" + )] #[test_matrix( 1,// step_count 0,// decrease_per_interval_numerator @@ -868,17 +908,43 @@ mod block_based_perpetual_step_decreasing { 1; // distribution_interval "no decrease, irrelevant min values" )] - #[test_matrix( - [1],// step_count + /// Given 100% decrease with step 10, when I claim below 10th block, then the claim is successful. + #[test_case( + 10,// step_count 1,// decrease_per_interval_numerator 1,// decrease_per_interval_denominator None,// s 100_000,// n None,// min_value - Some(vec![7,10,20,100]), // claim_heights // ,300,500,800,1_000,1_000_000 - [5]; // distribution_interval - "no decrease, changing step" + Some(vec![2,7,9]), // claim_heights // ,300,500,800,1_000,1_000_000 + 1; // distribution_interval + "full decrease, step 10 interval 1" + )] + /// Given 100% decrease with step 10 starting at 5, when I claim below 15th block, then the claim is successful. + #[test_case( + 10,// step_count + 1,// decrease_per_interval_numerator + 1,// decrease_per_interval_denominator + Some(5),// s + 100_000,// n + None,// min_value + Some(vec![2,7,9,13,14]), // claim_heights // ,300,500,800,1_000,1_000_000 + 1; // distribution_interval + "full decrease, start 5 step 10 interval 1" )] + /// Given 100% decrease with step 10 starting at 5, when I claim at height 15, there are no new coins. + #[test_case( + 10,// step_count + 1,// decrease_per_interval_numerator + 1,// decrease_per_interval_denominator + Some(5),// s + 100_000,// n + None,// min_value + Some(vec![14,15]), // claim_heights // at 14 we zero out, at 15 nothing to claim + 1 // distribution_interval + => with |x:Result<(),String>| assert!(x.is_err_and(|s|s.contains("claim at height 15: claim failed"))) + ;"full decrease, start 5 step 10 interval 1 err at 15" + )] #[test_matrix( [5,10],// step_count 1,// decrease_per_interval_numerator @@ -886,19 +952,19 @@ mod block_based_perpetual_step_decreasing { None,// s 100_000,// n None,// min_value - Some(vec![5,10,20,100]), // claim_heights // ,300,500,800,1_000,1_000_000 + Some(vec![5,10,18,22,100]), // claim_heights [1,5]; // distribution_interval - "1/2 decrease, changing step" + "fails: 1/2 decrease, changing step" )] #[test_matrix( - [1,10],// step_count - 1,// decrease_per_interval_numerator - 2,// decrease_per_interval_denominator + 1,// step_count + 10,// decrease_per_interval_numerator + 100,// decrease_per_interval_denominator [None,Some(1),Some(5)],// s 100_000,// n None,// min_value - Some(vec![5,10,100]), // claim_heights // ,300,500,800,1_000,1_000_000 - [1,5]; // distribution_interval + Some(vec![5,10,15,20]), // claim_heights // ,300,500,800,1_000,1_000_000 + 1; // distribution_interval "1/2 decrease, changing S" )] @@ -941,15 +1007,13 @@ mod block_based_perpetual_step_decreasing { }) .collect::>(); // we expect all tests to pass - let expect_pass = claim_heights.iter().map(|&_h| true).collect::>(); - let claims = claim_heights .iter() .zip(expected_balances.iter()) - .zip(expect_pass.iter()) - .map(|((&h, &b), &p)| (h, b, p)) + .map(|(&h, &b)| (h, b, true)) .collect::>(); + // we return Err(()) to make result comparision easier in test_case check_heights( dist, &claims, @@ -958,7 +1022,7 @@ mod block_based_perpetual_step_decreasing { None, ) .inspect_err(|e| { - tracing::error!("{}", e); + tracing::error!(e); }) } @@ -1023,7 +1087,7 @@ mod block_based_perpetual_stepwise { use std::collections::BTreeMap; #[test] - fn stepwise_correct() { + fn fails_stepwise_correct() { let periods = BTreeMap::from([ (0, 10_000), (20, 20_000), @@ -1037,16 +1101,20 @@ mod block_based_perpetual_stepwise { // 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), + (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), - (50, 180_000, true), - (51, 180_000, false), + (51, 180_000, true), + (52, 180_000, false), (70, 270_000, true), ( 1_000_000, @@ -1070,24 +1138,93 @@ mod block_based_perpetual_stepwise { } mod block_based_perpetual_linear { + use std::i64; + use super::{test_suite::check_heights, INITIAL_BALANCE}; use dpp::data_contract::associated_token::token_perpetual_distribution::distribution_function::DistributionFunction; use rust_decimal::prelude::ToPrimitive; - use test_case::test_case; + use test_case::{test_case, test_matrix}; - #[test_case(DistributionFunction::Linear{ - a: 1, - d: 1, - start_step: None, - starting_amount: 100_000, - min_value: None, - max_value: None - }, - &[10 ], - 1 - ; "x=y")] - - fn test_linear(dist: DistributionFunction, heights: &[u64], distribution_interval: u64) { + #[test_matrix( + 1,// a + 1, // d + [None,Some(0)], // start_step + 0, // starting_amount + [None,Some(0),Some(1)],// min_value + [None,Some(1000)],// max_value + &[(1,100_001,true),(2,100_003,true),(3,100_006,true),(10,100_055,true)], // heights + 1 // distribution_interval + ; "f(x)=x")] + + /// Given linear distribution with d=0, + /// When I create a token, + /// Then I get an error. + #[test_case( + 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 + ; "fails: divide by 0")] + /// 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_case( + 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 // distribution_interval + ; "divide by u64::MAX")] + #[test_matrix( + [-1,-100000,i64::MIN],// a + 1, // d + None, // start_step + 0, // starting_amount + None, // min_value + None, // max_value + &[(1,100_000,false),(20,100_000,false)], // heights + 1 // distribution_interval + ; "negative a")] + + /// We expect failure when max < min + #[test_matrix( + 1,// a + 1, // d + None, // start_step + 0, // starting_amount + Some(100), // min_value + [Some(0),Some(99)], // max_value + &[(1,100_000,false),(20,100_000,false)], // heights + 1 // distribution_interval + ; "fails: max less than min")] + #[test_case( + 1,// a + 1, // d + None, // start_step + 0, // starting_amount + Some(10), // min_value + Some(10), // max_value + &[(1,100_010,true),(2,100_020,true),(10,100_100,true)], // heights + 1 // distribution_interval + ; "min eq max")] + + 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, + ) { // Linear distribution function // // # Formula @@ -1097,17 +1234,27 @@ mod block_based_perpetual_linear { // f(x) = (a * (x - start_moment) / d) + starting_amount // ``` // - let steps = heights - .iter() - .scan((INITIAL_BALANCE, 1), |(balance, last_height), &h| { - for i in (*last_height..=h).step_by(distribution_interval as usize) { - *balance += expected_emission(i, 1, 1, None, 100_000); - } - *last_height = h; - - Some((h, *balance, true)) - }) - .collect::>(); + let dist = DistributionFunction::Linear { + a, + d, + start_step, + starting_amount, + min_value, + max_value, + }; + // let steps = heights + // .iter() + // .scan((INITIAL_BALANCE, 1), |(balance, last_height), &h| { + // if *last_height > start_step.unwrap_or(1) { + // for i in (*last_height..=h).step_by(distribution_interval as usize) { + // *balance += expected_emission(i, a, d, start_step, starting_amount); + // } + // } + // *last_height = h; + + // Some((h, *balance, true)) + // }) + // .collect::>(); check_heights( dist, &steps, @@ -1120,38 +1267,6 @@ mod block_based_perpetual_linear { }) .expect("stepwise should pass"); } - - /// Calculate expected emission at provided height. - /// - /// All calculations are done in i128 to better handle overflows. - /// - /// ```text - /// f(x) = (a * (x - start_moment) / d) + starting_amount - /// ``` - fn expected_emission( - x: u64, - a: u64, - d: u64, - start_moment: Option, - starting_amount: u64, - ) -> u64 { - let x = x as i128; - let a = a as i128; - let d = d as i128; - let start_moment = start_moment.unwrap_or(0) as i128; - let starting_amount = starting_amount as i128; - - let f_x = if x < start_moment { - starting_amount - } else { - (a * (x - start_moment) / d + starting_amount).max(0) - }; - - f_x.to_u64().unwrap_or_else(|| { - tracing::error!("overflow in expected_emission({}), using 0", f_x); - 0 - }) - } } mod test_suite { @@ -1189,6 +1304,9 @@ mod test_suite { .map_err(|e| format!("join error: {:?}", e))? } + pub(super) fn contains(a: T, text: &str) -> bool { + a.to_string().contains(text) + } /// 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. @@ -1235,9 +1353,7 @@ mod test_suite { suite = suite.with_max_suppy(max_supply); } - if let Some(start) = contract_start_time { - suite = suite.with_contract_start_time(start); - } + suite = suite.with_contract_start_time(contract_start_time.unwrap_or(1)); let steps = steps .iter() @@ -1482,7 +1598,8 @@ mod test_suite { pub(crate) fn get_balance(&mut self) -> Result, String> { let token_id = self.get_token_id().to_buffer(); - self.platform + let balance = self + .platform .drive .fetch_identity_token_balance( token_id, @@ -1490,7 +1607,10 @@ mod test_suite { None, self.platform_version, ) - .map_err(|e| format!("failed to fetch token balance: {}", e)) + .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. @@ -1618,6 +1738,11 @@ mod test_suite { } if result.is_empty() { + tracing::trace!( + "step successful, base height: {}, balance: {}", + test_case.base_height, + balance + ); (self.on_step_success)(balance); Ok(()) } else { From 3adc24c994cfe6514d8dee03cb99ad3763b65b88 Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Thu, 27 Mar 2025 14:02:20 +0100 Subject: [PATCH 10/28] chore(Dockerfile): snapshots_enabled = true --- Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile b/Dockerfile index 47e7e5a77fe..a688730baaf 100644 --- a/Dockerfile +++ b/Dockerfile @@ -598,6 +598,7 @@ RUN apk add --no-cache libgcc libstdc++ ENV DB_PATH=/var/lib/dash/rs-drive-abci/db ENV REJECTIONS_PATH=/var/log/dash/rejected +ENV SNAPSHOTS_ENABLED=true RUN mkdir -p /var/log/dash \ /var/lib/dash/rs-drive-abci/db \ From b84b54b3a286808b88ce0b6d003cb5be65f5279d Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Thu, 27 Mar 2025 14:15:28 +0100 Subject: [PATCH 11/28] Revert "chore(Dockerfile): snapshots_enabled = true" This reverts commit c8502a370fb2b1d9ded0d451c0dc7fa84037d65a. --- Dockerfile | 1 - 1 file changed, 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index a688730baaf..47e7e5a77fe 100644 --- a/Dockerfile +++ b/Dockerfile @@ -598,7 +598,6 @@ RUN apk add --no-cache libgcc libstdc++ ENV DB_PATH=/var/lib/dash/rs-drive-abci/db ENV REJECTIONS_PATH=/var/log/dash/rejected -ENV SNAPSHOTS_ENABLED=true RUN mkdir -p /var/log/dash \ /var/lib/dash/rs-drive-abci/db \ From 1061b52fd293fdb01cb91f9283ff1f57876916ea Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Thu, 3 Apr 2025 16:49:00 +0200 Subject: [PATCH 12/28] test: Polynomial --- .../distribution/perpetual/block_based.rs | 389 ++++++++++++++++-- 1 file changed, 362 insertions(+), 27 deletions(-) diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/perpetual/block_based.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/perpetual/block_based.rs index 6e60d3b6e34..086ee9db356 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 @@ -88,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, @@ -156,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, @@ -226,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, @@ -337,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, @@ -459,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, @@ -1138,12 +1138,9 @@ mod block_based_perpetual_stepwise { } mod block_based_perpetual_linear { - use std::i64; - - use super::{test_suite::check_heights, INITIAL_BALANCE}; + use super::test_suite::check_heights; use dpp::data_contract::associated_token::token_perpetual_distribution::distribution_function::DistributionFunction; - use rust_decimal::prelude::ToPrimitive; - use test_case::{test_case, test_matrix}; + use test_case::test_matrix; #[test_matrix( 1,// a @@ -1242,22 +1239,10 @@ mod block_based_perpetual_linear { min_value, max_value, }; - // let steps = heights - // .iter() - // .scan((INITIAL_BALANCE, 1), |(balance, last_height), &h| { - // if *last_height > start_step.unwrap_or(1) { - // for i in (*last_height..=h).step_by(distribution_interval as usize) { - // *balance += expected_emission(i, a, d, start_step, starting_amount); - // } - // } - // *last_height = h; - - // Some((h, *balance, true)) - // }) - // .collect::>(); + check_heights( dist, - &steps, + steps, None, //Some(S), distribution_interval, None, @@ -1269,6 +1254,358 @@ mod block_based_perpetual_linear { } } +mod block_based_perpetual_polynomial { + 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 crate::platform_types::state_transitions_processing_result::StateTransitionExecutionResult; + use super::test_suite::{check_heights, TestStep, TestSuite}; + + /// Calculates f(x) = (a * (x - s + o)^(m/n)) / d + b + + #[test_case::test_matrix([1,2,10,20])] + + fn test_fx(x_max: i128) { + let a: i128 = 1; + let d: i128 = 1; + let m: i128 = 1; + let n: i128 = 1; + let o: i128 = 1; + let s: i128 = 0; + let b: i128 = 100_000; + + let mut sum = 0; + for x in 1i128..=x_max { + // f(x) = (a * (x - s + o)^(m/n)) / d + b + let f_x = (a * (x - s + o).pow((m / n) as u32)) / d + b; + sum += f_x; + println!("f({}) = {}", x, f_x); + } + + println!("SUM({}) = {}", n, sum); + } + + #[test_case::test_case( + 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), + ], // steps + 1; // distribution_interval + "ones")] + + /// 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_case::test_case( + 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), + ], // steps + 1; // distribution_interval + "fails: divide by 0")] + #[test_case::test_case( + 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), + ], // steps + 1 // distribution_interval + ; "max < min should fail")] + #[test_case::test_case( + 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), + ], // steps + 1 // distribution_interval + ; "negative a")] + + #[test_case::test_case( + 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), + ], // steps + 1 // distribution_interval + ; "fails: a=i64::MIN")] + + #[test_case::test_case( + 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), + ], // steps + 1 // distribution_interval + ; "a=-1 b=0")] + + #[test_case::test_case( + 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), + ], // steps + 1 // distribution_interval + ; "o=i64::MIN")] + + #[test_case::test_case( + 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), + ], // steps + 1 // distribution_interval + ; "o=i64::MAX")] + + #[test_case::test_case( + 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), // this should fail, 0.pow(-1) is unspecified + (2,100_001,true), // it's 1.pow(-1) but not sure about handling of overflow at prev height + ], // steps + 1 // distribution_interval + ; "0.pow(-1) at h=1")] + + #[test_case::test_case( + 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 + ], // steps + 1 // distribution_interval + ; "0.pow(1/2) at h=1")] + + #[test_case::test_case( + 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), + ], // steps + 1 // distribution_interval + ; "fails: o=i64::MAX m=2")] + /// 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_case::test_matrix( + [i64::MIN,0,1,i64::MAX],// m + [0,1,u64::MAX] // n + ; "power m/n" + )] + // due to bug in test_matrix https://github.com/frondeus/test-case/issues/19, we need separate test for -1 + #[test_case::test_matrix( + -1,// m + [0,1,u64::MAX] // n + ; "negative power -1/n" + )] + /// Test various combinations of `m/n` in [DistributionFunction::Polynomial] distribution. + /// + /// We expect this test not to end with InternalError. + fn test_poynomial_power(m: i64, n: u64) { + 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); + }) + .expect("test should pass"); + } +} mod test_suite { use super::*; use crate::rpc::core::MockCoreRPCLike; @@ -1304,9 +1641,7 @@ mod test_suite { .map_err(|e| format!("join error: {:?}", e))? } - pub(super) fn contains(a: T, text: &str) -> bool { - a.to_string().contains(text) - } + /// 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. From fe48a9e584cc04624295be1cf43735f258634f0c Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Fri, 4 Apr 2025 12:41:46 +0200 Subject: [PATCH 13/28] test: Logarithmic --- .../distribution/perpetual/block_based.rs | 278 ++++++++++++++++-- 1 file changed, 258 insertions(+), 20 deletions(-) diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/perpetual/block_based.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/perpetual/block_based.rs index 086ee9db356..daa1e733f5a 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 @@ -952,7 +952,7 @@ mod block_based_perpetual_step_decreasing { None,// s 100_000,// n None,// min_value - Some(vec![5,10,18,22,100]), // claim_heights + Some(vec![5,10,18,22,100]), // claim_heights [1,5]; // distribution_interval "fails: 1/2 decrease, changing step" )] @@ -1255,6 +1255,8 @@ mod block_based_perpetual_linear { } mod block_based_perpetual_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, @@ -1270,8 +1272,6 @@ mod block_based_perpetual_polynomial { }, TokenConfiguration, }; - use crate::platform_types::state_transitions_processing_result::StateTransitionExecutionResult; - use super::test_suite::{check_heights, TestStep, TestSuite}; /// Calculates f(x) = (a * (x - s + o)^(m/n)) / d + b @@ -1367,13 +1367,12 @@ mod block_based_perpetual_polynomial { min_value: None, max_value: None, }, - &[ + &[ (1,199_999,true), (4,499_990,true), ], // steps 1 // distribution_interval ; "negative a")] - #[test_case::test_case( Polynomial { a: i64::MIN, @@ -1392,7 +1391,6 @@ mod block_based_perpetual_polynomial { ], // steps 1 // distribution_interval ; "fails: a=i64::MIN")] - #[test_case::test_case( Polynomial { a: -1, @@ -1405,13 +1403,12 @@ mod block_based_perpetual_polynomial { min_value: None, max_value: None, }, - &[ + &[ (1,100_000,false), (4,100_000,false), ], // steps 1 // distribution_interval ; "a=-1 b=0")] - #[test_case::test_case( Polynomial { a: 1, @@ -1424,13 +1421,12 @@ mod block_based_perpetual_polynomial { min_value: None, max_value: None, }, - &[ + &[ (1,100_000,false), (4,100_000,false), ], // steps 1 // distribution_interval ; "o=i64::MIN")] - #[test_case::test_case( Polynomial { a: 1, @@ -1443,13 +1439,12 @@ mod block_based_perpetual_polynomial { min_value: None, max_value: None, }, - &[ + &[ (1,100_000,false), (4,100_000,false), ], // steps 1 // distribution_interval ; "o=i64::MAX")] - #[test_case::test_case( Polynomial { a: 1, @@ -1462,13 +1457,12 @@ mod block_based_perpetual_polynomial { 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) but not sure about handling of overflow at prev height ], // steps 1 // distribution_interval - ; "0.pow(-1) at h=1")] - + ; "0.pow(-1) at h=1")] #[test_case::test_case( Polynomial { a: 1, @@ -1481,7 +1475,7 @@ mod block_based_perpetual_polynomial { 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 @@ -1491,7 +1485,6 @@ mod block_based_perpetual_polynomial { ], // steps 1 // distribution_interval ; "0.pow(1/2) at h=1")] - #[test_case::test_case( Polynomial { a: 1, @@ -1504,9 +1497,9 @@ mod block_based_perpetual_polynomial { min_value: None, max_value: None, }, - &[ + &[ (1,100_000,false), - (10,100_000,false), + (10,100_000,false), ], // steps 1 // distribution_interval ; "fails: o=i64::MAX m=2")] @@ -1606,6 +1599,252 @@ mod block_based_perpetual_polynomial { .expect("test should pass"); } } +mod block_based_perpetual_logarithmic { + + use super::test_suite::check_heights; + use dpp::data_contract::associated_token::token_perpetual_distribution::distribution_function::DistributionFunction::{self,Logarithmic}; + use test_case::{test_matrix,test_case}; + + #[test_case( + 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 + ; "fails: ones - use of ln instead of log as documented" + )] + #[test_case( + 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 + ; "fails: divide by 0" + )] + #[test_case( + 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 + ; "fails: log(0)" + )] + #[test_case( + 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 + ; "min eq max means linear" + )] + #[test_case( + 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 + ; "min eq max means linear, interval 5" + )] + #[test_case( + 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 + ; "fails: min gt max" + )] + #[test_case( + 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, + }, + &[ + (1,100_000,false), // f(1) should be < 0, is 1 + (9,100_000,false), + (10,100_000,false) + ], + 1 + ; "fails: a=i64::MIN" + )] + #[test_case( + 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 + ; "fails: a=i64::MAX overflows" + )] + #[test_case( + 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 + ; "a=0 b=0" + )] + #[test_case( + 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 + ; "fails: log(negative)" + )] + #[test_case( + 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 + ; "fails: o=i64::MIN" + )] + #[test_case( + 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 + ; "fails: b=u64::MAX" + )] + /// 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 test_suite { use super::*; use crate::rpc::core::MockCoreRPCLike; @@ -1641,7 +1880,6 @@ mod test_suite { .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. From 9045516980ff88b7aff714a422d143cb23917780 Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Fri, 4 Apr 2025 13:41:26 +0200 Subject: [PATCH 14/28] test: InvertedLogarithmic --- .../distribution/perpetual/block_based.rs | 279 +++++++++++++++++- 1 file changed, 278 insertions(+), 1 deletion(-) 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 daa1e733f5a..193a44921f1 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 @@ -1599,12 +1599,28 @@ mod block_based_perpetual_polynomial { .expect("test should pass"); } } + mod block_based_perpetual_logarithmic { use super::test_suite::check_heights; use dpp::data_contract::associated_token::token_perpetual_distribution::distribution_function::DistributionFunction::{self,Logarithmic}; use test_case::{test_matrix,test_case}; - + #[test_case( + 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 + ; "zeros" + )] #[test_case( Logarithmic{ a: 1, // a: i64, @@ -1845,6 +1861,267 @@ mod block_based_perpetual_logarithmic { } } +mod block_based_perpetual_inverted_logarithmic { + use super::test_suite::check_heights; + use dpp::data_contract::associated_token::token_perpetual_distribution::distribution_function::DistributionFunction::{self,InvertedLogarithmic}; + use test_case::{test_matrix,test_case}; + + #[test_case( + 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, + }, + &[(4,100_000,true)], + 1 + ; "fails: zeros" + )] + #[test_case( + 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, + }, + &[ + (1,100_001,true), + (2,100_002,true), + (3,100_003,true), + (4,100_005,true), // [InternalError("storage: protocol: divide by zero error: InvertedLogarithmic: divisor d is 0")] + ], + 1 + ; "fails: ones" + )] + #[test_case( + 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, + }, + &[(2,100_002,false)], + 1 + ; "fails: divide by 0" + )] + #[test_case( + 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, + }, + &[(1,100_001,true),(5,100_001,true)], + 1 + ; "n=0 log(0)" + )] + #[test_case( + 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, + }, + &[(1,100_010,true),(5,100_050,true)], + 1 + ; "min eq max means linear" + )] + #[test_case( + 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, + }, + &[(5,100_010,true),(10,100_020,true)], + 5 + ; "min eq max means linear, interval 5" + )] + #[test_case( + 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, + }, + &[(5,100_000,false),(10,100_000,false)], + 5 + ; "fails: min gt max" + )] + #[test_case( + 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, + }, + &[ + (1,100_000,false), // f(1) should be < 0, is 1 + (9,100_000,false), + (10,100_000,false) + ], + 1 + ; "fails: a=i64::MIN" + )] + #[test_case( + 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, + }, + &[ + (1,100_001,true), // f(x) = 0 for x>1 + (9,100_001,false), + (10,100_001,false), + ], + 1 + ; "a=i64::MAX" + )] + #[test_case( + 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, + }, + &[ + (1,100_000,false), + (9,100_000,false), + (10,100_000,false) + ], + 1 + ; "a=0 b=0" + )] + #[test_case( + 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, + }, + &[ + (1,100_000,false), + (9,100_000,false), + (10,100_000,false) + ], + 1 + ; "fails: log(negative)" + )] + #[test_case( + 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, + }, + &[ + (1,100_000,false), + (9,100_000,false), + (10,100_000,false) + ], + 1 + ; "fails: o=i64::MIN" + )] + #[test_case( + 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, + }, + &[ + (1,100_000,false), + (9,100_000,false), + (10,100_000,false) + ], + 1 + ; "fails: b=u64::MAX" + )] + /// f(x) = (a * log( n / (m * (x - s + o)) )) / d + b + fn test_inverted_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 test_suite { use super::*; use crate::rpc::core::MockCoreRPCLike; From 3e9810d7fb078623dc30e40b88814a6189b57355 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Sun, 6 Apr 2025 20:52:14 +0700 Subject: [PATCH 15/28] fix: saturate instead of wrap on pseudorandom function --- .../distribution_function/evaluate.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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); From e25b027915e42f02d926e8f46b66f332344b9051 Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Mon, 7 Apr 2025 14:15:31 +0200 Subject: [PATCH 16/28] test: validate distribution fn before use --- .../distribution/perpetual/block_based.rs | 40 +++++++++++++++++-- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/perpetual/block_based.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/perpetual/block_based.rs index 193a44921f1..94871fb4125 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 @@ -2129,6 +2129,8 @@ mod test_suite { 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; @@ -2200,7 +2202,7 @@ mod test_suite { }), ); if let Some(max_supply) = max_supply { - suite = suite.with_max_suppy(max_supply); + suite = suite.with_max_supply(max_supply); } suite = suite.with_contract_start_time(contract_start_time.unwrap_or(1)); @@ -2309,7 +2311,7 @@ mod test_suite { self } /// Appends a token configuration modification that will change max supply. - pub(crate) fn with_max_suppy(self, max_supply: Option) -> Self { + 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); @@ -2339,7 +2341,16 @@ mod test_suite { // 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!("failed to validate distribution function: {}", e); + }; }; Some(closure) } else { @@ -2354,6 +2365,7 @@ mod test_suite { None, self.platform_version, ); + self.token_id = Some(token_id); self.contract = Some(contract.clone()); @@ -2418,7 +2430,7 @@ mod test_suite { .platform .platform .process_raw_state_transitions( - &vec![claim_serialized_transition.clone()], + &[claim_serialized_transition.clone()], &platform_state, &new_block_info, &transaction, @@ -2601,6 +2613,28 @@ mod test_suite { } } + /// 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!("distribution function validation failed: {:?}", e))?; + + Ok(()) + } + pub(crate) type AssertionFn = fn(&[StateTransitionExecutionResult]) -> Result<(), String>; /// Individual step of a test case. From 99cd113b56e7fa990d7e76283960acd831fcfc3d Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Mon, 7 Apr 2025 14:16:27 +0200 Subject: [PATCH 17/28] tmp: test_1_percent_increase_claim to prove issue --- .../distribution/perpetual/block_based.rs | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) 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 94871fb4125..852efc6064b 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 @@ -1026,6 +1026,42 @@ mod block_based_perpetual_step_decreasing { }) } + /// Just a workaround for lack of support in JetBrains IDE for this test + /// which starts "fails: 1% increase, claim every 500 blocks" test. + /// + /// TODO: remove when not needed anymore + #[test] + fn test_1_percent_increase_claim() { + // first, we run test claiming every 100 blocks + run_test( + 1, + 101, + 100, + None, + 100_000, + Some(1), + Some((1..1000).step_by(100).collect()), + 1, + ) + .expect("claiming every 100 blocks should pass"); + + // The same, but claim every 500 blocks, fails: + // + // * when we claim every 100 blocks, at height 501 we get 100_510 + // * when we claim every 500 blocks, at height 501 we get 100_138 + run_test( + 1, + 101, + 100, + None, + 100_000, + Some(1), + Some((1..1000).step_by(500).collect()), + 1, + ) + .expect("claiming every 500 blocks should also pass, but it fails"); + } + // ===== HELPER FUNCTIONS ===== // /// Calculate expected emission at provided height. From ac4cbc929bf9b52d17020f5d2de3d115dabff1d9 Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Mon, 7 Apr 2025 19:02:08 +0200 Subject: [PATCH 18/28] test: test max_token_redemption_cycles --- .../distribution/perpetual/block_based.rs | 142 ++++++++++++------ 1 file changed, 94 insertions(+), 48 deletions(-) diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/perpetual/block_based.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/perpetual/block_based.rs index 852efc6064b..b055218133c 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 @@ -814,12 +814,22 @@ mod block_based_perpetual_random { mod block_based_perpetual_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 test_case::test_case; - 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::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_case( 1,// step_count 1,// decrease_per_interval_numerator @@ -831,17 +841,6 @@ mod block_based_perpetual_step_decreasing { 1; // distribution_interval "claim every 100 blocks" )] - #[test_case( - 1,// step_count - 1,// decrease_per_interval_numerator - 100,// decrease_per_interval_denominator - None,// s - 100_000,// n - Some(1),// min_value - Some((1..1000).step_by(500).collect()),// claim_heights - 1; // distribution_interval - "fails: claim every 500 blocks" - )] #[test_case( 1,// step_count 101,// decrease_per_interval_numerator @@ -861,8 +860,9 @@ mod block_based_perpetual_step_decreasing { 100_000,// n Some(1),// min_value Some((1..1000).step_by(500).collect()),// claim_heights - 1; // distribution_interval - "fails: 1% increase, claim every 500 blocks" + 1 // distribution_interval + => with |x:Result<(),String>| assert!(x.is_err_and(|s|s.contains("claim at height 501: expected balance Some(100510) but got 100138"))) + ; "1% increase, claim every 500 blocks fails due to max_token_redemption_cycles" )] #[test_case( 1,// step_count @@ -1026,40 +1026,85 @@ mod block_based_perpetual_step_decreasing { }) } - /// Just a workaround for lack of support in JetBrains IDE for this test - /// which starts "fails: 1% increase, claim every 500 blocks" test. - /// - /// TODO: remove when not needed anymore + /// 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_1_percent_increase_claim() { - // first, we run test claiming every 100 blocks - run_test( - 1, - 101, - 100, - None, - 100_000, - Some(1), - Some((1..1000).step_by(100).collect()), - 1, - ) - .expect("claiming every 100 blocks should pass"); + 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), + }; - // The same, but claim every 500 blocks, fails: - // - // * when we claim every 100 blocks, at height 501 we get 100_510 - // * when we claim every 500 blocks, at height 501 we get 100_138 - run_test( - 1, - 101, - 100, - None, - 100_000, - Some(1), - Some((1..1000).step_by(500).collect()), + let dist_clone = dist.clone(); + let mut suite = TestSuite::new( + 10_200_000_000, 1, - ) - .expect("claiming every 500 blocks should also pass, but it fails"); + 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 ===== // @@ -1498,6 +1543,7 @@ mod block_based_perpetual_polynomial { (2,100_001,true), // it's 1.pow(-1) but not sure about handling of overflow at prev height ], // steps 1 // distribution_interval + => with |x:Result<(),String>| assert!(x.is_err_and(|s|s.contains("invalid distribution function: Overflow"))) ; "0.pow(-1) at h=1")] #[test_case::test_case( Polynomial { @@ -1520,7 +1566,7 @@ mod block_based_perpetual_polynomial { (6,100_008,true), // 5.pow(1/2) == 2.23 - should round to 2 ], // steps 1 // distribution_interval - ; "0.pow(1/2) at h=1")] + ; "0.pow(1/2) at h=1 fails dist fn validation")] #[test_case::test_case( Polynomial { a: 1, @@ -2385,7 +2431,7 @@ mod test_suite { token_configuration, self.start_time.unwrap_or(0), ) { - panic!("failed to validate distribution function: {}", e); + panic!("{}", e); }; }; Some(closure) @@ -2666,7 +2712,7 @@ mod test_suite { .distribution_type .function() .validate(contract_start_time) - .map_err(|e| format!("distribution function validation failed: {:?}", e))?; + .map_err(|e| format!("invalid distribution function: {:?}", e))?; Ok(()) } From 962af284176b9ce3809ad1031ca70ea0f036a5b1 Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Tue, 8 Apr 2025 15:06:03 +0200 Subject: [PATCH 19/28] test: refactor - don't use test_case crate --- Cargo.lock | 1 - packages/rs-drive-abci/Cargo.toml | 7 +- .../distribution/perpetual/block_based.rs | 2040 +++++++++-------- 3 files changed, 1063 insertions(+), 985 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a1eb8fe3328..0fb070fb49d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1611,7 +1611,6 @@ dependencies = [ "strategy-tests", "tempfile", "tenderdash-abci", - "test-case", "thiserror 1.0.64", "tokio", "tokio-util", diff --git a/packages/rs-drive-abci/Cargo.toml b/packages/rs-drive-abci/Cargo.toml index 6c315df9677..984eff52982 100644 --- a/packages/rs-drive-abci/Cargo.toml +++ b/packages/rs-drive-abci/Cargo.toml @@ -103,8 +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" } -test-case = { version = "3.3.1" } - # For tests of grovedb verify rocksdb = { version = "0.23.0" } integer-encoding = { version = "4.0.0" } @@ -121,4 +119,7 @@ name = "drive-abci" path = "src/main.rs" [lints.rust] -unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tokio_unstable)', 'cfg(create_sdk_test_data)'] } +unexpected_cfgs = { level = "warn", check-cfg = [ + 'cfg(tokio_unstable)', + 'cfg(create_sdk_test_data)', +] } 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 b055218133c..e96acd7f0cb 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 @@ -649,32 +649,30 @@ mod block_based_perpetual_random { }, TokenConfiguration, }; - use test_case::test_matrix; - /// 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), - ] - )] - fn test_random(min: u64, max: u64, max_supply: Option, steps: &[TestStep]) { - check_heights( - DistributionFunction::Random { min, max }, - steps, - None, - 10, - Some(max_supply), - ) - .expect("correct case 1"); + ]; + + 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, @@ -824,151 +822,166 @@ mod block_based_perpetual_step_decreasing { use dpp::data_contract::associated_token::token_perpetual_distribution::TokenPerpetualDistribution; use dpp::data_contract::TokenConfiguration; use rust_decimal::prelude::ToPrimitive; - use test_case::test_case; 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_case( - 1,// step_count - 1,// decrease_per_interval_numerator - 100,// decrease_per_interval_denominator - None,// s - 100_000,// n - Some(1),// min_value - Some((1..1000).step_by(100).collect()),// claim_heights - 1; // distribution_interval - "claim every 100 blocks" - )] - #[test_case( - 1,// step_count - 101,// decrease_per_interval_numerator - 100,// decrease_per_interval_denominator - None,// s - 100_000,// n - Some(1),// min_value - Some((1..1000).step_by(100).collect()),// claim_heights - 1; // distribution_interval - "1% increase, claim every 100 blocks" - )] - #[test_case( - 1,// step_count - 101,// decrease_per_interval_numerator - 100,// decrease_per_interval_denominator - None,// s - 100_000,// n - Some(1),// min_value - Some((1..1000).step_by(500).collect()),// claim_heights - 1 // distribution_interval - => with |x:Result<(),String>| assert!(x.is_err_and(|s|s.contains("claim at height 501: expected balance Some(100510) but got 100138"))) - ; "1% increase, claim every 500 blocks fails due to max_token_redemption_cycles" - )] - #[test_case( - 1,// step_count - 1000,// decrease_per_interval_numerator - 1,// decrease_per_interval_denominator - None,// s - 100_000,// n - Some(1),// min_value - Some(vec![1,7]), // claim_heights - 1; // distribution_interval - "fails: 1000x increase, overflow" - )] - #[test_matrix( - 1,// step_count - 1,// decrease_per_interval_numerator - 1,// decrease_per_interval_denominator - None,// s - 100_000,// n - [Some(1), Some(100)],// min_value - Some(vec![1,2,3,10,100]), // claim_heights // ,300,500,800,1_000,1_000_000 - 1; // distribution_interval - "100% decrease, various min values" - )] - #[test_case( - 1,// step_count - 1,// decrease_per_interval_numerator - 1,// decrease_per_interval_denominator - None,// s - 100_000,// n - Some(u64::MAX),// min_value - Some(vec![1,2,3,10,100]), // claim_heights // ,300,500,800,1_000,1_000_000 - 1; // distribution_interval - "fails: full decrease, min is u64::MAX" - )] - #[test_matrix( - 1,// step_count - 0,// decrease_per_interval_numerator - 1,// decrease_per_interval_denominator - None,// s - 100_000,// n - [None,Some(0),Some(1),Some(100)],// min_value - Some(vec![1,2,3,10,100]), // claim_heights // ,300,500,800,1_000,1_000_000 - 1; // distribution_interval - "no decrease, irrelevant min values" - )] - /// Given 100% decrease with step 10, when I claim below 10th block, then the claim is successful. - #[test_case( - 10,// step_count - 1,// decrease_per_interval_numerator - 1,// decrease_per_interval_denominator - None,// s - 100_000,// n - None,// min_value - Some(vec![2,7,9]), // claim_heights // ,300,500,800,1_000,1_000_000 - 1; // distribution_interval - "full decrease, step 10 interval 1" - )] - /// Given 100% decrease with step 10 starting at 5, when I claim below 15th block, then the claim is successful. - #[test_case( - 10,// step_count - 1,// decrease_per_interval_numerator - 1,// decrease_per_interval_denominator - Some(5),// s - 100_000,// n - None,// min_value - Some(vec![2,7,9,13,14]), // claim_heights // ,300,500,800,1_000,1_000_000 - 1; // distribution_interval - "full decrease, start 5 step 10 interval 1" - )] - /// Given 100% decrease with step 10 starting at 5, when I claim at height 15, there are no new coins. - #[test_case( - 10,// step_count - 1,// decrease_per_interval_numerator - 1,// decrease_per_interval_denominator - Some(5),// s - 100_000,// n - None,// min_value - Some(vec![14,15]), // claim_heights // at 14 we zero out, at 15 nothing to claim - 1 // distribution_interval - => with |x:Result<(),String>| assert!(x.is_err_and(|s|s.contains("claim at height 15: claim failed"))) - ;"full decrease, start 5 step 10 interval 1 err at 15" - )] - #[test_matrix( - [5,10],// step_count - 1,// decrease_per_interval_numerator - 2,// decrease_per_interval_denominator - None,// s - 100_000,// n - None,// min_value - Some(vec![5,10,18,22,100]), // claim_heights - [1,5]; // distribution_interval - "fails: 1/2 decrease, changing step" - )] - #[test_matrix( - 1,// step_count - 10,// decrease_per_interval_numerator - 100,// decrease_per_interval_denominator - [None,Some(1),Some(5)],// s - 100_000,// n - None,// min_value - Some(vec![5,10,15,20]), // claim_heights // ,300,500,800,1_000,1_000_000 - 1; // distribution_interval - "1/2 decrease, changing S" - )] + #[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, @@ -1221,78 +1234,107 @@ mod block_based_perpetual_stepwise { mod block_based_perpetual_linear { use super::test_suite::check_heights; use dpp::data_contract::associated_token::token_perpetual_distribution::distribution_function::DistributionFunction; - use test_case::test_matrix; - - #[test_matrix( - 1,// a - 1, // d - [None,Some(0)], // start_step - 0, // starting_amount - [None,Some(0),Some(1)],// min_value - [None,Some(1000)],// max_value - &[(1,100_001,true),(2,100_003,true),(3,100_006,true),(10,100_055,true)], // heights - 1 // distribution_interval - ; "f(x)=x")] /// Given linear distribution with d=0, /// When I create a token, /// Then I get an error. - #[test_case( - 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 - ; "fails: divide by 0")] + #[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_case( - 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 // distribution_interval - ; "divide by u64::MAX")] - #[test_matrix( - [-1,-100000,i64::MIN],// a - 1, // d - None, // start_step - 0, // starting_amount - None, // min_value - None, // max_value - &[(1,100_000,false),(20,100_000,false)], // heights - 1 // distribution_interval - ; "negative a")] - - /// We expect failure when max < min - #[test_matrix( - 1,// a - 1, // d - None, // start_step - 0, // starting_amount - Some(100), // min_value - [Some(0),Some(99)], // max_value - &[(1,100_000,false),(20,100_000,false)], // heights - 1 // distribution_interval - ; "fails: max less than min")] - #[test_case( - 1,// a - 1, // d - None, // start_step - 0, // starting_amount - Some(10), // min_value - Some(10), // max_value - &[(1,100_010,true),(2,100_020,true),(10,100_100,true)], // heights - 1 // distribution_interval - ; "min eq max")] + #[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, @@ -1302,7 +1344,7 @@ mod block_based_perpetual_linear { max_value: Option, steps: &[(u64, u64, bool)], // height, expected balance, expect pass distribution_interval: u64, - ) { + ) -> Result<(), String> { // Linear distribution function // // # Formula @@ -1331,7 +1373,6 @@ mod block_based_perpetual_linear { .inspect_err(|e| { tracing::error!("{}", e); }) - .expect("stepwise should pass"); } } @@ -1354,71 +1395,50 @@ mod block_based_perpetual_polynomial { TokenConfiguration, }; - /// Calculates f(x) = (a * (x - s + o)^(m/n)) / d + b - - #[test_case::test_matrix([1,2,10,20])] - - fn test_fx(x_max: i128) { - let a: i128 = 1; - let d: i128 = 1; - let m: i128 = 1; - let n: i128 = 1; - let o: i128 = 1; - let s: i128 = 0; - let b: i128 = 100_000; - - let mut sum = 0; - for x in 1i128..=x_max { - // f(x) = (a * (x - s + o)^(m/n)) / d + b - let f_x = (a * (x - s + o).pow((m / n) as u32)) / d + b; - sum += f_x; - println!("f({}) = {}", x, f_x); - } - - println!("SUM({}) = {}", n, sum); + #[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, + ) } - #[test_case::test_case( - 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), - ], // steps - 1; // distribution_interval - "ones")] - /// 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_case::test_case( - 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), - ], // steps - 1; // distribution_interval - "fails: divide by 0")] - #[test_case::test_case( + #[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, + ) + } + + #[test] + fn max_lt_min_should_fail() -> Result<(), String> { + test_polynomial( Polynomial { a: 1, d: 1, @@ -1430,161 +1450,167 @@ mod block_based_perpetual_polynomial { 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, + ) + } + + #[test] + fn 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] + fn zero_pow_minus_1_at_h_1() -> Result<(), String> { + 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, + ) + } + #[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, + }, &[ - (10,100_000,false), - (20,100_000,false), - ], // steps - 1 // distribution_interval - ; "max < min should fail")] - #[test_case::test_case( - 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), - ], // steps - 1 // distribution_interval - ; "negative a")] - #[test_case::test_case( - 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), - ], // steps - 1 // distribution_interval - ; "fails: a=i64::MIN")] - #[test_case::test_case( - 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), - ], // steps - 1 // distribution_interval - ; "a=-1 b=0")] - #[test_case::test_case( - 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), - ], // steps - 1 // distribution_interval - ; "o=i64::MIN")] - #[test_case::test_case( - 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), - ], // steps - 1 // distribution_interval - ; "o=i64::MAX")] - #[test_case::test_case( - 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), // this should fail, 0.pow(-1) is unspecified - (2,100_001,true), // it's 1.pow(-1) but not sure about handling of overflow at prev height - ], // steps - 1 // distribution_interval - => with |x:Result<(),String>| assert!(x.is_err_and(|s|s.contains("invalid distribution function: Overflow"))) - ; "0.pow(-1) at h=1")] - #[test_case::test_case( - 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 - ], // steps - 1 // distribution_interval - ; "0.pow(1/2) at h=1 fails dist fn validation")] - #[test_case::test_case( - 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), - ], // steps - 1 // distribution_interval - ; "fails: o=i64::MAX m=2")] + (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 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` @@ -1605,80 +1631,78 @@ mod block_based_perpetual_polynomial { }) } - #[test_case::test_matrix( - [i64::MIN,0,1,i64::MAX],// m - [0,1,u64::MAX] // n - ; "power m/n" - )] - // due to bug in test_matrix https://github.com/frondeus/test-case/issues/19, we need separate test for -1 - #[test_case::test_matrix( - -1,// m - [0,1,u64::MAX] // n - ; "negative power -1/n" - )] /// Test various combinations of `m/n` in [DistributionFunction::Polynomial] distribution. /// /// We expect this test not to end with InternalError. - fn test_poynomial_power(m: i64, n: u64) { - let dist = Polynomial { - a: 1, - d: 1, - m, - n, - o: 1, - start_moment: Some(1), - b: 100_000, - min_value: None, - max_value: None, - }; + #[test] + fn test_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, - }, - ))); - }), - ); + 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); + 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(), + }; - 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))?; + } + } - suite - .execute(&[step]) - .inspect_err(|e| { - tracing::error!("{}", e); - }) - .expect("test should pass"); + Ok(()) } } @@ -1686,244 +1710,274 @@ mod block_based_perpetual_logarithmic { use super::test_suite::check_heights; use dpp::data_contract::associated_token::token_perpetual_distribution::distribution_function::DistributionFunction::{self,Logarithmic}; - use test_case::{test_matrix,test_case}; - #[test_case( - 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 - ; "zeros" - )] - #[test_case( - 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 - ; "fails: ones - use of ln instead of log as documented" - )] - #[test_case( - 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 - ; "fails: divide by 0" - )] - #[test_case( - 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 - ; "fails: log(0)" - )] - #[test_case( - 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 - ; "min eq max means linear" - )] - #[test_case( - 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 - ; "min eq max means linear, interval 5" - )] - #[test_case( - 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 - ; "fails: min gt max" - )] - #[test_case( - 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, - }, - &[ - (1,100_000,false), // f(1) should be < 0, is 1 - (9,100_000,false), - (10,100_000,false) - ], - 1 - ; "fails: a=i64::MIN" - )] - #[test_case( - 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 - ; "fails: a=i64::MAX overflows" - )] - #[test_case( - 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 - ; "a=0 b=0" - )] - #[test_case( - 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 - ; "fails: log(negative)" - )] - #[test_case( - 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 - ; "fails: o=i64::MIN" - )] - #[test_case( - 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 - ; "fails: b=u64::MAX" - )] + + #[test] + fn 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, + }, + &[ + (1, 100_000, false), // f(1) should be < 0, is 1 + (9, 100_000, false), + (10, 100_000, false), + ], + 1, + ) + } + #[test] + fn 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, @@ -1946,247 +2000,271 @@ mod block_based_perpetual_logarithmic { mod block_based_perpetual_inverted_logarithmic { use super::test_suite::check_heights; use dpp::data_contract::associated_token::token_perpetual_distribution::distribution_function::DistributionFunction::{self,InvertedLogarithmic}; - use test_case::{test_matrix,test_case}; - - #[test_case( - 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, - }, - &[(4,100_000,true)], - 1 - ; "fails: zeros" - )] - #[test_case( - 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, - }, - &[ - (1,100_001,true), - (2,100_002,true), - (3,100_003,true), - (4,100_005,true), // [InternalError("storage: protocol: divide by zero error: InvertedLogarithmic: divisor d is 0")] - ], - 1 - ; "fails: ones" - )] - #[test_case( - 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, - }, - &[(2,100_002,false)], - 1 - ; "fails: divide by 0" - )] - #[test_case( - 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, - }, - &[(1,100_001,true),(5,100_001,true)], - 1 - ; "n=0 log(0)" - )] - #[test_case( - 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, - }, - &[(1,100_010,true),(5,100_050,true)], - 1 - ; "min eq max means linear" - )] - #[test_case( - 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, - }, - &[(5,100_010,true),(10,100_020,true)], - 5 - ; "min eq max means linear, interval 5" - )] - #[test_case( - 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, - }, - &[(5,100_000,false),(10,100_000,false)], - 5 - ; "fails: min gt max" - )] - #[test_case( - 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, - }, - &[ - (1,100_000,false), // f(1) should be < 0, is 1 - (9,100_000,false), - (10,100_000,false) - ], - 1 - ; "fails: a=i64::MIN" - )] - #[test_case( - 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, - }, - &[ - (1,100_001,true), // f(x) = 0 for x>1 - (9,100_001,false), - (10,100_001,false), - ], - 1 - ; "a=i64::MAX" - )] - #[test_case( - 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, - }, - &[ - (1,100_000,false), - (9,100_000,false), - (10,100_000,false) - ], - 1 - ; "a=0 b=0" - )] - #[test_case( - 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, - }, - &[ - (1,100_000,false), - (9,100_000,false), - (10,100_000,false) - ], - 1 - ; "fails: log(negative)" - )] - #[test_case( - 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, - }, - &[ - (1,100_000,false), - (9,100_000,false), - (10,100_000,false) - ], - 1 - ; "fails: o=i64::MIN" - )] - #[test_case( - 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, - }, - &[ - (1,100_000,false), - (9,100_000,false), - (10,100_000,false) - ], - 1 - ; "fails: b=u64::MAX" - )] + + #[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 fails_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 fails_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 test_inverted_logarithmic( + fn run_test( dist: DistributionFunction, steps: &[(u64, u64, bool)], // height, expected balance, expect pass distribution_interval: u64, From 967be6202801ce7e2ec13ea83367965f16f38290 Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Tue, 8 Apr 2025 15:16:18 +0200 Subject: [PATCH 20/28] test: simplify mod names --- .../token/distribution/perpetual/block_based.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/perpetual/block_based.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/perpetual/block_based.rs index e96acd7f0cb..acd043145d5 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 @@ -501,7 +501,7 @@ mod perpetual_distribution_block { } #[cfg(test)] -mod block_based_perpetual_fixed_amount { +mod fixed_amount { use crate::platform_types::state_transitions_processing_result::StateTransitionExecutionResult; use super::{test_suite::*, INITIAL_BALANCE}; @@ -623,7 +623,7 @@ mod block_based_perpetual_fixed_amount { .expect("\nfixed amount u64::MAX should pass\n"); } } -mod block_based_perpetual_random { +mod random { use std::{ collections::BTreeMap, sync::{Arc, Mutex}, @@ -810,7 +810,7 @@ mod block_based_perpetual_random { } } -mod block_based_perpetual_step_decreasing { +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; @@ -1175,7 +1175,7 @@ mod block_based_perpetual_step_decreasing { } } -mod block_based_perpetual_stepwise { +mod stepwise { use super::test_suite::check_heights; use dpp::data_contract::associated_token::token_perpetual_distribution::distribution_function::DistributionFunction; use std::collections::BTreeMap; @@ -1231,7 +1231,7 @@ mod block_based_perpetual_stepwise { } } -mod block_based_perpetual_linear { +mod linear { use super::test_suite::check_heights; use dpp::data_contract::associated_token::token_perpetual_distribution::distribution_function::DistributionFunction; @@ -1376,7 +1376,7 @@ mod block_based_perpetual_linear { } } -mod block_based_perpetual_polynomial { +mod polynomial { use super::test_suite::{check_heights, TestStep, TestSuite}; use crate::platform_types::state_transitions_processing_result::StateTransitionExecutionResult; use dpp::data_contract::{ @@ -1706,7 +1706,7 @@ mod block_based_perpetual_polynomial { } } -mod block_based_perpetual_logarithmic { +mod logarithmic { use super::test_suite::check_heights; use dpp::data_contract::associated_token::token_perpetual_distribution::distribution_function::DistributionFunction::{self,Logarithmic}; @@ -1997,7 +1997,7 @@ mod block_based_perpetual_logarithmic { } } -mod block_based_perpetual_inverted_logarithmic { +mod inverted_logarithmic { use super::test_suite::check_heights; use dpp::data_contract::associated_token::token_perpetual_distribution::distribution_function::DistributionFunction::{self,InvertedLogarithmic}; From c0ac2bf269e8a0a548c13b221b5fc36e649eafb5 Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Tue, 8 Apr 2025 17:40:13 +0200 Subject: [PATCH 21/28] test: further improvements --- .../distribution/perpetual/block_based.rs | 69 ++++++++++++++----- 1 file changed, 52 insertions(+), 17 deletions(-) diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/perpetual/block_based.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/perpetual/block_based.rs index acd043145d5..15c858e6c1d 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 @@ -510,14 +510,31 @@ mod fixed_amount { 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 test_block_based_perpetual_fixed_amount_50() { + 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), @@ -537,7 +554,7 @@ mod fixed_amount { /// claim at height 1000000000000: claim failed: assertion 0 failed: expected SuccessfulExecution, /// got [InternalError(\"storage: protocol: overflow error: Overflow in FixedAmount evaluation\")]" #[test] - fn fail_test_block_based_perpetual_fixed_amount_1_000_000_000() { + fn fail_fixed_amount_1_000_000_000() { check_heights( DistributionFunction::FixedAmount { amount: 1_000_000_000, @@ -564,7 +581,7 @@ mod fixed_amount { /// Given a fixed amount distribution with value of 0, /// When we try to claim, /// Then we always fail and the balance remains unchanged. - fn test_block_based_perpetual_fixed_amount_0() { + fn fixed_amount_0() { check_heights( DistributionFunction::FixedAmount { amount: 0 }, &[ @@ -584,7 +601,7 @@ mod fixed_amount { /// 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 test_fixed_amount_above_max_supply() { + fn fixed_amount_gt_max_supply() { let test = TestStep { name: "test_fixed_amount_above_max_supply".to_string(), base_height: 41, @@ -1182,16 +1199,16 @@ mod stepwise { #[test] fn fails_stepwise_correct() { + let distribution_interval = 10; let periods = BTreeMap::from([ - (0, 10_000), - (20, 20_000), + (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); - let distribution_interval = 10; // claims: height, balance, expect_pass let steps = [ @@ -1436,8 +1453,11 @@ mod polynomial { ) } + /// Given max_value < min_value, + /// When I try to use the token distribution function, + /// Then the token distribution function validation fails. #[test] - fn max_lt_min_should_fail() -> Result<(), String> { + fn fails_max_lt_min_should_fail() -> Result<(), String> { test_polynomial( Polynomial { a: 1, @@ -1511,8 +1531,11 @@ mod polynomial { ) } + /// 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 o_min() -> Result<(), String> { + fn fails_o_min() -> Result<(), String> { test_polynomial( Polynomial { a: 1, @@ -1550,7 +1573,8 @@ mod polynomial { } #[test] - fn zero_pow_minus_1_at_h_1() -> Result<(), String> { + #[should_panic(expected = "invalid distribution function")] + fn zero_pow_minus_1_at_h_1_invalid() { test_polynomial( Polynomial { a: 1, @@ -1566,6 +1590,8 @@ mod polynomial { &[(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> { @@ -1594,7 +1620,7 @@ mod polynomial { } #[test] - fn o_max_m_2() -> Result<(), String> { + fn fails_o_max_m_2() -> Result<(), String> { test_polynomial( Polynomial { a: 1, @@ -1635,7 +1661,7 @@ mod polynomial { /// /// We expect this test not to end with InternalError. #[test] - fn test_poynomial_power() -> Result<(), String> { + 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 { @@ -1712,7 +1738,7 @@ mod logarithmic { use dpp::data_contract::associated_token::token_perpetual_distribution::distribution_function::DistributionFunction::{self,Logarithmic}; #[test] - fn zeros() -> Result<(), String> { + fn fails_zeros() -> Result<(), String> { test_logarithmic( Logarithmic { a: 0, // a: i64, @@ -1860,16 +1886,23 @@ mod logarithmic { 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), // f(1) should be < 0, is 1 - (9, 100_000, false), - (10, 100_000, false), + (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 a_max_overflows() -> Result<(), String> { + fn fails_a_max_overflows() -> Result<(), String> { test_logarithmic( Logarithmic { a: i64::MAX, // a: i64, @@ -2511,6 +2544,8 @@ mod test_suite { ) { panic!("{}", e); }; + + tracing::trace!("token configuration validated"); }; Some(closure) } else { From b55e618e93a3c53aa29754c14aa553c607f99205 Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Tue, 8 Apr 2025 17:50:32 +0200 Subject: [PATCH 22/28] test: minor fix --- .../batch/tests/token/distribution/perpetual/block_based.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/perpetual/block_based.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/perpetual/block_based.rs index 15c858e6c1d..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 @@ -2188,7 +2188,7 @@ mod inverted_logarithmic { } #[test] - fn fails_a_max() -> Result<(), String> { + fn a_max() -> Result<(), String> { let dist = InvertedLogarithmic { a: i64::MAX, // a: i64, d: 1, // d: u64, @@ -2210,7 +2210,7 @@ mod inverted_logarithmic { } #[test] - fn fails_a_zero_b_zero() -> Result<(), String> { + fn a_zero_b_zero() -> Result<(), String> { let dist = InvertedLogarithmic { a: 0, // a: i64, d: 1, // d: u64, From 3c04663c79d6dc6560d825a22a0d82663edc85ee Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Mon, 14 Apr 2025 15:38:43 +0700 Subject: [PATCH 23/28] max distribution cycles --- .../distribution_function/mod.rs | 7 +++++++ .../reward_distribution_type/mod.rs | 19 +++++++++++-------- .../distribution/perpetual/block_based.rs | 17 ++++++++++++----- 3 files changed, 30 insertions(+), 13 deletions(-) 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..0439e3b8466 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,13 @@ mod validation; pub const MAX_DISTRIBUTION_PARAM: u64 = 281_474_976_710_655; //u48::Max 2^48 - 1 +/// The max cycles param is the upper limit of cycles the system can ever support +/// This is applied to linear distribution. +/// For all other distributions we use a versioned max cycles contained in the platform version. +/// That other version is much lower because the calculations for other distributions are more +/// complex. +pub const MAX_DISTRIBUTION_CYCLES_PARAM: u64 = 32_767; //u15::Max 2^(63 - 48) - 1 + pub const MAX_LINEAR_SLOPE_PARAM: u64 = 256; #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd)] 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..b15722df0ae 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,6 +509,7 @@ 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; #[test] fn fixed_amount_1_interval_1() -> Result<(), String> { @@ -531,7 +532,7 @@ mod fixed_amount { // Then the claim should 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), @@ -549,12 +550,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 +564,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, From f28a0136a41e250c27ece783441b642b92132393 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Tue, 15 Apr 2025 15:26:32 +0700 Subject: [PATCH 24/28] more fixes --- .../distribution_function/encode.rs | 22 +- .../distribution_function/evaluate.rs | 67 ++- .../distribution_function/mod.rs | 32 +- .../distribution_function/validation.rs | 47 +- .../distribution/perpetual/block_based.rs | 526 +++++++++++++----- 5 files changed, 515 insertions(+), 179 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..25404dfae12 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 @@ -23,7 +23,9 @@ impl Encode for DistributionFunction { decrease_per_interval_numerator, decrease_per_interval_denominator, s, - n, + 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, 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, + 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..8b6f126ba4e 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 { @@ -52,41 +54,50 @@ impl DistributionFunction { decrease_per_interval_numerator, decrease_per_interval_denominator, s, - n, + 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", - )); + + 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 as u128; + let denominator = *decrease_per_interval_denominator as u128; + let reduction_numerator = denominator - *decrease_per_interval_numerator as u128; + + for _ in 0..steps_passed { + numerator = numerator * reduction_numerator / denominator; + } + + let mut result = numerator as u64; + + 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 @@ -502,7 +513,7 @@ mod tests { decrease_per_interval_numerator: 1, decrease_per_interval_denominator: 2, // 50% reduction per step s: Some(0), - n: 100, + distribution_start_amount: 100, min_value: Some(10), }; @@ -521,7 +532,7 @@ mod tests { decrease_per_interval_numerator: 1, decrease_per_interval_denominator: 0, // Invalid denominator s: Some(0), - n: 100, + distribution_start_amount: 100, 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 0439e3b8466..d1bf4b8477c 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 @@ -18,6 +18,8 @@ pub const MAX_DISTRIBUTION_PARAM: u64 = 281_474_976_710_655; //u48::Max 2^48 - 1 /// 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)] @@ -98,7 +100,11 @@ pub enum DistributionFunction { /// - `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. + /// - `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 @@ -113,7 +119,9 @@ pub enum DistributionFunction { decrease_per_interval_numerator: u16, decrease_per_interval_denominator: u16, s: Option, - n: TokenAmount, + max_interval_count: Option, + distribution_start_amount: TokenAmount, + trailing_distribution_interval_amount: TokenAmount, min_value: Option, }, @@ -538,22 +546,34 @@ impl fmt::Display for DistributionFunction { decrease_per_interval_numerator, decrease_per_interval_denominator, s, - n, + 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..83b1c43bd3d 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(), @@ -61,21 +61,50 @@ impl DistributionFunction { decrease_per_interval_numerator, decrease_per_interval_denominator, s, - n, + 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(), @@ -97,7 +126,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(), @@ -940,7 +969,7 @@ mod tests { decrease_per_interval_numerator: 1, decrease_per_interval_denominator: 2, s: Some(0), - n: 100, + distribution_start_amount: 100, min_value: Some(10), }; let result = dist.validate(START_MOMENT); @@ -957,7 +986,7 @@ mod tests { decrease_per_interval_numerator: 1, decrease_per_interval_denominator: 2, s: Some(0), - n: 100, + distribution_start_amount: 100, min_value: Some(10), }; let result = dist.validate(START_MOMENT); @@ -974,7 +1003,7 @@ mod tests { decrease_per_interval_numerator: 1, decrease_per_interval_denominator: 0, s: Some(0), - n: 100, + distribution_start_amount: 100, min_value: Some(10), }; let result = dist.validate(START_MOMENT); 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 b15722df0ae..1e6b98ee594 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,11 +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; + 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), @@ -528,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() { 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), @@ -591,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] @@ -636,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)], @@ -644,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 { @@ -659,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, @@ -718,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, @@ -845,80 +878,304 @@ mod step_decreasing { 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::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, + ]; + + #[test] + fn claim_every_block() { + run_test( + 1, + 1, + 100, + None, + None, + 10_000, + 0, + Some(1), + (1..5).step_by(1).collect(), + 1, + vec![ + INITIAL_BALANCE + 9_900, + INITIAL_BALANCE + 9_900 + 9_801, + INITIAL_BALANCE + 9_900 + 9_801 + 9_702, + INITIAL_BALANCE + 9_900 + 9_801 + 9_702 + 9_604, + ], + ) + .expect("expected to succeed"); + } + #[test] - fn claim_every_100_blocks() -> Result<(), String> { + fn claim_every_5_blocks() { run_test( 1, 1, 100, None, - 100_000, + None, + 10_000, + 0, Some(1), - Some((1..1000).step_by(100).collect()), + 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_every_100_blocks_with_1_percent_increase() -> Result<(), String> { + fn claim_every_100_blocks_with_1_percent_increase() { run_test( 1, 101, 100, None, + None, + 100_000, + 0, + Some(1), + (1..1000).step_by(100).collect(), + 1, + vec![], + ) + .expect_err("should not allow to increase"); + } + + 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() + } + + #[test] + fn claim_every_10_blocks_on_100k() { + let steps = (1..500).step_by(10).collect::>(); + run_test( + 1, + 1, + 100, + None, + Some(1024), 100_000, + 0, Some(1), - Some((1..1000).step_by(100).collect()), + steps.clone(), 1, + sum_till_for_100k_step_1_interval_1(steps), ) + .expect("should pass"); } #[test] - fn claim_every_500_blocks_fails_due_to_max_token_redemption_cycles() -> Result<(), String> { - let result = run_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, - 101, 100, None, + None, 100_000, + 0, Some(1), - Some((1..1000).step_by(500).collect()), + steps.clone(), 1, - ); - assert!(result.is_err_and( - |s| s.contains("claim at height 501: expected balance Some(100510) but got 100138") - )); - Ok(()) + expected_amounts, + ) + .expect("should pass"); } #[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_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 full_decrease_min_1_100() -> Result<(), String> { + fn claim_every_10_blocks_on_100k_128_default_steps() { + let steps = (1..500).step_by(10).collect::>(); + let start_steps = (1..128).step_by(10).collect::>(); + let start_steps_expected_amounts = sum_till_for_100k_step_1_interval_1(start_steps); + let step_128_amount = sum_till_for_100k_step_1_interval_1(vec![128]).remove(0); + let later_steps = (141..500).step_by(10).collect::>(); + let later_steps_expected_amounts = later_steps.iter().map(|_| step_128_amount).collect::>(); + let mut expected_amounts = start_steps_expected_amounts; + expected_amounts.push(step_128_amount); // at 131. + expected_amounts.extend(later_steps_expected_amounts); + run_test( + 1, + 1, + 100, + None, + None, + 100_000, + 0, + Some(1), + steps.clone(), + 1, + expected_amounts, + ) + .expect("should pass"); + } + + #[test] + fn claim_128_default_steps_480_max_token_redemption_cycles() { + // We can only claim 128 events at a time. + // The step_wise distribution stops after 500 from the start. + let claim_heights = vec![1, 400, 400, 400, 400, 401, 450, 500]; + // 129 is the first claim for 400 because we can only do 128 cycles at a time + // Then 257 because we are doing 128 cycles and 129 + 128 = 257 + // The last one is 480 because our max steps is 480 + let expected_amounts = sum_till_for_100k_step_1_interval_1(vec![1, 129, 257, 385, 400, 401, 450, 480]); + run_test( + 1, + 1, + 100, + None, + Some(480), + 100_000, + 0, + Some(1), + // This will give us 1, 151, 301, 400, 401, 450 for result values + claim_heights, + 1, + expected_amounts, + ) + .expect("should pass"); + } + + #[test] + fn decrease_where_min_would_not_matter_min_1_100() { + let claim_heights = vec![1, 2, 3, 10, 100]; + let expected_amounts = sum_till_for_100k_step_1_interval_1(claim_heights.clone()); for min in [1, 100] { run_test( 1, 1, - 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] @@ -928,24 +1185,51 @@ mod step_decreasing { 1, 1, None, + None, 100_000, + 0, Some(u64::MAX), - Some(vec![1, 2, 3, 10, 100]), + vec![1, 2, 3, 10, 100], 1, + vec![], ) } #[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))?; + run_test( + 1, + 0, + 1, + None, + None, + 100_000, + 0, + min, + vec![1, 2, 3, 10, 100], + 1, + vec![], + ) + .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) + run_test( + 10, + 1, + 1, + None, + None, + 100_000, + 0, + None, + vec![2, 7, 9], + 1, + vec![], + ) } #[test] @@ -955,16 +1239,31 @@ mod step_decreasing { 1, 1, Some(5), + None, 100_000, + 0, None, - Some(vec![2, 7, 9, 13, 14]), + vec![2, 7, 9, 13, 14], 1, + vec![], ) } #[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); + let result = run_test( + 10, + 1, + 1, + Some(5), + None, + 100_000, + 0, + None, + vec![14, 15], + 1, + vec![], + ); assert!(result.is_err_and(|s| s.contains("claim at height 15: claim failed"))); Ok(()) } @@ -978,10 +1277,13 @@ mod step_decreasing { 1, 2, None, + None, 100_000, + 0, None, - Some(vec![5, 10, 18, 22, 100]), + vec![5, 10, 18, 22, 100], distribution_interval, + vec![], ) .map_err(|e| { format!( @@ -998,8 +1300,20 @@ mod step_decreasing { #[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))?; + run_test( + 1, + 10, + 100, + s, + None, + 100_000, + 0, + None, + vec![5, 10, 15, 20], + 1, + vec![], + ) + .map_err(|e| format!("failed with s {:?}: {}", s, e))?; } Ok(()) } @@ -1010,47 +1324,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, + s: 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, + 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, + None => true, + }; + 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, @@ -1073,7 +1385,9 @@ mod step_decreasing { decrease_per_interval_numerator: 101, decrease_per_interval_denominator: 100, s: None, - n: 100_000, + max_interval_count: None, + distribution_start_amount: 100_000, + trailing_distribution_interval_amount: 0, min_value: Some(1), }; @@ -1143,60 +1457,6 @@ mod step_decreasing { ); } } - - // ===== 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 { @@ -2828,12 +3088,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 018bef2ad3508b3192374faff0616e08fe6e1085 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Tue, 15 Apr 2025 19:33:16 +0700 Subject: [PATCH 25/28] more work --- .../distribution_function/encode.rs | 6 +- .../distribution_function/evaluate.rs | 8 +- .../distribution_function/mod.rs | 7 +- .../distribution_function/validation.rs | 19 +- .../distribution/perpetual/block_based.rs | 432 ++++++++++-------- 5 files changed, 258 insertions(+), 214 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 25404dfae12..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,7 +22,7 @@ impl Encode for DistributionFunction { step_count, decrease_per_interval_numerator, decrease_per_interval_denominator, - s, + start_decreasing_offset: s, max_interval_count, distribution_start_amount: n, trailing_distribution_interval_amount, @@ -176,7 +176,7 @@ impl Decode for DistributionFunction { 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, @@ -329,7 +329,7 @@ impl<'de> BorrowDecode<'de> for DistributionFunction { step_count, decrease_per_interval_numerator, decrease_per_interval_denominator, - s, + start_decreasing_offset: s, max_interval_count, distribution_start_amount: n, trailing_distribution_interval_amount, 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 8b6f126ba4e..3758608531b 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 @@ -53,7 +53,7 @@ impl DistributionFunction { step_count, decrease_per_interval_numerator, decrease_per_interval_denominator, - s, + start_decreasing_offset, max_interval_count, distribution_start_amount, trailing_distribution_interval_amount, @@ -65,7 +65,7 @@ impl DistributionFunction { )); } - let s_val = s.unwrap_or(contract_registration_step); + let s_val = start_decreasing_offset.unwrap_or(contract_registration_step); if x <= s_val { return Ok(*distribution_start_amount); @@ -512,7 +512,7 @@ mod tests { step_count: 10, decrease_per_interval_numerator: 1, decrease_per_interval_denominator: 2, // 50% reduction per step - s: Some(0), + start_decreasing_offset: Some(0), distribution_start_amount: 100, min_value: Some(10), }; @@ -531,7 +531,7 @@ mod tests { step_count: 10, decrease_per_interval_numerator: 1, decrease_per_interval_denominator: 0, // Invalid denominator - s: Some(0), + start_decreasing_offset: Some(0), distribution_start_amount: 100, 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 d1bf4b8477c..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 @@ -99,7 +99,8 @@ 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. + /// - `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. @@ -118,7 +119,7 @@ pub enum DistributionFunction { step_count: u32, decrease_per_interval_numerator: u16, decrease_per_interval_denominator: u16, - s: Option, + start_decreasing_offset: Option, max_interval_count: Option, distribution_start_amount: TokenAmount, trailing_distribution_interval_amount: TokenAmount, @@ -545,7 +546,7 @@ impl fmt::Display for DistributionFunction { step_count, decrease_per_interval_numerator, decrease_per_interval_denominator, - s, + start_decreasing_offset: s, max_interval_count, distribution_start_amount, trailing_distribution_interval_amount, 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 83b1c43bd3d..be07457a7fd 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 @@ -60,7 +60,7 @@ impl DistributionFunction { step_count, decrease_per_interval_numerator, decrease_per_interval_denominator, - s, + start_decreasing_offset: s, max_interval_count, distribution_start_amount, trailing_distribution_interval_amount, @@ -110,6 +110,17 @@ impl DistributionFunction { 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(), @@ -968,7 +979,7 @@ mod tests { step_count: 10, decrease_per_interval_numerator: 1, decrease_per_interval_denominator: 2, - s: Some(0), + start_decreasing_offset: Some(0), distribution_start_amount: 100, min_value: Some(10), }; @@ -985,7 +996,7 @@ mod tests { step_count: 0, decrease_per_interval_numerator: 1, decrease_per_interval_denominator: 2, - s: Some(0), + start_decreasing_offset: Some(0), distribution_start_amount: 100, min_value: Some(10), }; @@ -1002,7 +1013,7 @@ mod tests { step_count: 10, decrease_per_interval_numerator: 1, decrease_per_interval_denominator: 0, - s: Some(0), + start_decreasing_offset: Some(0), distribution_start_amount: 100, min_value: Some(10), }; 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 1e6b98ee594..014edf45c29 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 @@ -872,7 +872,7 @@ mod step_decreasing { 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_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::v0::TokenPerpetualDistributionV0; @@ -923,6 +923,67 @@ mod step_decreasing { 628, 621, 614, ]; + fn sum_till_for_100k_step_1_interval_1( + distribution_heights: Vec, + ) -> Vec { + distribution_heights + .into_iter() + .map(|height| { + (1..=height) + .map(|height| DECREASING_ONE_PERCENT_100K[height as usize]) + .sum::() + + INITIAL_BALANCE + }) + .collect() + } + + const DECREASING_HALF_100K: [TokenAmount; 20] = [ + 100000, 50000, 25000, 12500, 6250, 3125, 1562, 781, 390, 195, 97, 48, 24, 12, 6, 3, 1, 0, + 0, 0, + ]; + + fn sum_till_for_100k_halving( + distribution_heights: Vec, + reduce_every_block_count: u32, + interval: BlockHeightInterval, + start_decreasing_step: u64, + ) -> Vec { + distribution_heights + .into_iter() + .map(|height| { + // How many full intervals have passed by `height`? + let end = height / interval; + + // If not even 1 interval, return the initial balance + if end < 1 { + return INITIAL_BALANCE; + } + + // Sum each interval’s distribution + let sum_halved = (1..=end) + .map(|i| { + if i < start_decreasing_step { + // Before start offset => always distribute the first entry + DECREASING_HALF_100K[0] + } else { + // After offset => normal indexing + let offset_index = + ((i - start_decreasing_step) as usize) + / (reduce_every_block_count as usize); + + DECREASING_HALF_100K + .get(offset_index) + .copied() + .unwrap_or(0) + } + }) + .sum::(); + + INITIAL_BALANCE + sum_halved + }) + .collect() + } + #[test] fn claim_every_block() { run_test( @@ -980,8 +1041,8 @@ mod step_decreasing { } #[test] - fn claim_every_100_blocks_with_1_percent_increase() { - run_test( + fn claim_with_1_percent_increase_should_fail() { + let result_str = run_test( 1, 101, 100, @@ -995,20 +1056,26 @@ mod step_decreasing { vec![], ) .expect_err("should not allow to increase"); + assert_eq!(result_str, "join error: JoinError::Panic(Id(3), \"Invalid parameter tuple in token distribution function: `decrease_per_interval_numerator` must be smaller than `decrease_per_interval_denominator`\", ...)"); } - 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() + #[test] + fn claim_with_no_decrease_should_fail() { + let result_str = run_test( + 1, + 0, + 100, + None, + None, + 100_000, + 0, + Some(1), + (1..1000).step_by(100).collect(), + 1, + vec![], + ) + .expect_err("should not allow to increase"); + assert_eq!(result_str, "join error: JoinError::Panic(Id(3), \"Invalid parameter `decrease_per_interval_numerator` in token distribution function. Expected range: 1 to 65535\", ...)"); } #[test] @@ -1036,7 +1103,10 @@ mod step_decreasing { 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 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( @@ -1052,7 +1122,7 @@ mod step_decreasing { 1, expected_amounts, ) - .expect("should pass"); + .expect("should pass"); } #[test] @@ -1061,7 +1131,10 @@ mod step_decreasing { 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 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( @@ -1078,7 +1151,7 @@ mod step_decreasing { 1, expected_amounts, ) - .expect("should pass"); + .expect("should pass"); } #[test] @@ -1088,7 +1161,10 @@ mod step_decreasing { 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 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); @@ -1105,7 +1181,7 @@ mod step_decreasing { 1, expected_amounts, ) - .expect("should pass"); + .expect("should pass"); } #[test] @@ -1116,7 +1192,8 @@ mod step_decreasing { // 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]); + let expected_amounts = + sum_till_for_100k_step_1_interval_1(vec![1, 129, 257, 385, 400, 401, 450, 480]); run_test( 1, 1, @@ -1152,7 +1229,8 @@ mod step_decreasing { 1, expected_amounts.clone(), ) - .map_err(|e| format!("failed with min {}: {}", min, e)).expect("should pass"); + .map_err(|e| format!("failed with min {}: {}", min, e)) + .expect("should pass"); } } @@ -1160,7 +1238,13 @@ mod step_decreasing { 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]; + 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, @@ -1174,148 +1258,179 @@ mod step_decreasing { 1, expected_amounts, ) - .map_err(|e| format!("failed with min {}: {}", min, e)).expect("should pass"); + .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, None, - 100_000, + MAX_DISTRIBUTION_PARAM, 0, Some(u64::MAX), vec![1, 2, 3, 10, 100], 1, vec![], ) + .expect_err("should fail"); + assert_eq!(result_str, "join error: JoinError::Panic(Id(3), \"Invalid parameter tuple in token distribution function: `n` must be greater than or equal to `min_value`\", ...)"); } #[test] - fn no_decrease_changing_min() -> Result<(), String> { - for min in [None, Some(0), Some(1), Some(100)] { - run_test( - 1, - 0, - 1, - None, - None, - 100_000, - 0, - min, - vec![1, 2, 3, 10, 100], - 1, - vec![], - ) - .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> { + 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( - 10, + 1, + u16::MAX - 1, + u16::MAX, + None, + None, + MAX_DISTRIBUTION_PARAM, + MAX_DISTRIBUTION_PARAM, + Some(MAX_DISTRIBUTION_PARAM), + claim_heights, + 1, + expected_balances, + ) + .expect("should succeed"); + } + + #[test] + fn start_over_max_distribution_param_should_fail() { + let result_str = run_test( 1, 1, + u16::MAX, None, None, - 100_000, + MAX_DISTRIBUTION_PARAM + 1, 0, None, - vec![2, 7, 9], + vec![1, 2, 10], 1, vec![], ) + .expect_err("should fail"); + assert_eq!(result_str, "join error: JoinError::Panic(Id(3), \"Invalid parameter `n` in token distribution function. Expected range: 1 to 281474976710655\", ...)"); } #[test] - fn full_decrease_start_5_step_10_interval_1() -> Result<(), String> { + 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( - 10, - 1, + step, 1, - Some(5), + 2, + None, None, 100_000, 0, None, - vec![2, 7, 9, 13, 14], - 1, - vec![], + claim_heights, + distribution_interval, + expected_balances, ) + .expect("should pass"); } #[test] - fn full_decrease_start_5_step_10_interval_1_err_at_15() -> Result<(), String> { - let result = run_test( - 10, - 1, + 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, - Some(5), + 2, + None, None, 100_000, 0, None, - vec![14, 15], - 1, - vec![], - ); - assert!(result.is_err_and(|s| s.contains("claim at height 15: claim failed"))); - Ok(()) + 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, - None, - 100_000, - 0, - None, - vec![5, 10, 18, 22, 100], - distribution_interval, - vec![], - ) - .map_err(|e| { - format!( - "failed with step {} interval {}: {}", - step, distribution_interval, 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_chainging_s() -> Result<(), String> { - for s in [None, Some(1), Some(5)] { - run_test( - 1, - 10, - 100, - s, - None, - 100_000, - 0, - None, - vec![5, 10, 15, 20], - 1, - vec![], - ) - .map_err(|e| format!("failed with s {:?}: {}", s, e))?; - } - Ok(()) + 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. @@ -1324,7 +1439,7 @@ mod step_decreasing { step_count: u32, decrease_per_interval_numerator: u16, decrease_per_interval_denominator: u16, - s: Option, + start_decreasing_offset: Option, max_interval_count: Option, distribution_start_amount: TokenAmount, trailing_distribution_interval_amount: TokenAmount, @@ -1337,7 +1452,7 @@ mod step_decreasing { step_count, decrease_per_interval_numerator, decrease_per_interval_denominator, - s, + start_decreasing_offset, max_interval_count, distribution_start_amount, trailing_distribution_interval_amount, @@ -1355,7 +1470,7 @@ mod step_decreasing { .map(|(&h, &b)| { let is_increase = match prev { Some(p) => b > p, - None => true, + None => b > INITIAL_BALANCE, }; prev = Some(b); (h, b, is_increase) @@ -1374,89 +1489,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, - max_interval_count: None, - distribution_start_amount: 100_000, - trailing_distribution_interval_amount: 0, - 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 - ); - } - } } mod stepwise { From efc37dbe816f0ab0a2395ddb152187bc9bc8e955 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Tue, 15 Apr 2025 20:48:08 +0700 Subject: [PATCH 26/28] step decreasing distribution fixes --- .../distribution_function/evaluate.rs | 8 +- .../distribution/perpetual/block_based.rs | 91 ++++++++++++------- 2 files changed, 61 insertions(+), 38 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 3758608531b..b40d04e18f4 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 @@ -80,15 +80,15 @@ impl DistributionFunction { return Ok(*trailing_distribution_interval_amount); } - let mut numerator = *distribution_start_amount as u128; - let denominator = *decrease_per_interval_denominator as u128; - let reduction_numerator = denominator - *decrease_per_interval_numerator as u128; + let mut numerator = *distribution_start_amount; + let denominator = *decrease_per_interval_denominator as u64; + let reduction_numerator = denominator - *decrease_per_interval_numerator as u64; for _ in 0..steps_passed { numerator = numerator * reduction_numerator / denominator; } - let mut result = numerator as u64; + let mut result = numerator; if let Some(min) = min_value { if result < *min { 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 014edf45c29..a7fa630a613 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 @@ -869,21 +869,11 @@ 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, 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::v0::TokenPerpetualDistributionV0; - use dpp::data_contract::associated_token::token_perpetual_distribution::TokenPerpetualDistribution; - use dpp::data_contract::TokenConfiguration; 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, @@ -967,14 +957,10 @@ mod step_decreasing { 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) + 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::(); @@ -1056,7 +1042,10 @@ mod step_decreasing { vec![], ) .expect_err("should not allow to increase"); - assert_eq!(result_str, "join error: JoinError::Panic(Id(3), \"Invalid parameter tuple in token distribution function: `decrease_per_interval_numerator` must be smaller than `decrease_per_interval_denominator`\", ...)"); + 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] @@ -1075,7 +1064,10 @@ mod step_decreasing { vec![], ) .expect_err("should not allow to increase"); - assert_eq!(result_str, "join error: JoinError::Panic(Id(3), \"Invalid parameter `decrease_per_interval_numerator` in token distribution function. Expected range: 1 to 65535\", ...)"); + assert!( + result_str.contains("Invalid parameter `decrease_per_interval_numerator` in token distribution function. Expected range: 1 to 65535"), + "Unexpected panic message: {result_str}" + ); } #[test] @@ -1279,7 +1271,10 @@ mod step_decreasing { vec![], ) .expect_err("should fail"); - assert_eq!(result_str, "join error: JoinError::Panic(Id(3), \"Invalid parameter tuple in token distribution function: `n` must be greater than or equal to `min_value`\", ...)"); + assert!( + result_str.contains("Invalid parameter tuple in token distribution function: `n` must be greater than or equal to `min_value`"), + "Unexpected panic message: {result_str}" + ); } #[test] fn full_decrease_min_eq_max_distribution() { @@ -1306,7 +1301,15 @@ mod step_decreasing { #[test] fn distribute_max_distribution_param_every_step() { let claim_heights = (1..65_536).step_by(128).collect::>(); - let expected_balances = claim_heights.iter().map(|&height| MAX_DISTRIBUTION_PARAM.saturating_mul(height).saturating_add(INITIAL_BALANCE).min(i64::MAX as u64)).collect(); + 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, @@ -1320,7 +1323,7 @@ mod step_decreasing { 1, expected_balances, ) - .expect("should succeed"); + .expect("should succeed"); } #[test] @@ -1339,7 +1342,10 @@ mod step_decreasing { vec![], ) .expect_err("should fail"); - assert_eq!(result_str, "join error: JoinError::Panic(Id(3), \"Invalid parameter `n` in token distribution function. Expected range: 1 to 281474976710655\", ...)"); + assert!( + result_str.contains("Invalid parameter `n` in token distribution function. Expected range: 1 to 281474976710655"), + "Unexpected panic message: {result_str}" + ); } #[test] @@ -1347,7 +1353,8 @@ mod step_decreasing { 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); + let expected_balances = + sum_till_for_100k_halving(claim_heights.clone(), step, distribution_interval, 0); run_test( step, 1, @@ -1368,8 +1375,9 @@ mod step_decreasing { 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); + 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, @@ -1383,7 +1391,7 @@ mod step_decreasing { distribution_interval, expected_balances, ) - .expect("should pass"); + .expect("should pass"); } #[test] @@ -1392,7 +1400,8 @@ mod step_decreasing { 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); + let expected_balances = + sum_till_for_100k_halving(value_heights, step, distribution_interval, 0); run_test( step, 1, @@ -1406,7 +1415,7 @@ mod step_decreasing { distribution_interval, expected_balances, ) - .expect("should pass"); + .expect("should pass"); } #[test] @@ -1415,8 +1424,22 @@ mod step_decreasing { 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); + 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, @@ -1430,7 +1453,7 @@ mod step_decreasing { distribution_interval, expected_balances, ) - .expect("should pass"); + .expect("should pass"); } /// Test various combinations of [DistributionFunction::StepDecreasingAmount] distribution. @@ -1469,7 +1492,7 @@ mod step_decreasing { .zip(expected_balances.iter()) .map(|(&h, &b)| { let is_increase = match prev { - Some(p) => b > p, + Some(p) => b > p || b == i64::MAX as u64, None => b > INITIAL_BALANCE, }; prev = Some(b); From 62fa908f88c8877301d06df0bffeeff84565a51a Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Tue, 15 Apr 2025 21:16:59 +0700 Subject: [PATCH 27/28] more fixes --- .../distribution_function/evaluate.rs | 6 +++++- .../distribution_function/validation.rs | 8 +++++++- 2 files changed, 12 insertions(+), 2 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 b40d04e18f4..b67586f6259 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 @@ -82,7 +82,7 @@ impl DistributionFunction { let mut numerator = *distribution_start_amount; let denominator = *decrease_per_interval_denominator as u64; - let reduction_numerator = denominator - *decrease_per_interval_numerator as u64; + let reduction_numerator = denominator.saturating_sub(*decrease_per_interval_numerator as u64); for _ in 0..steps_passed { numerator = numerator * reduction_numerator / denominator; @@ -513,7 +513,9 @@ mod tests { decrease_per_interval_numerator: 1, decrease_per_interval_denominator: 2, // 50% reduction per step start_decreasing_offset: Some(0), + max_interval_count: None, distribution_start_amount: 100, + trailing_distribution_interval_amount: 0, min_value: Some(10), }; @@ -532,7 +534,9 @@ mod tests { decrease_per_interval_numerator: 1, decrease_per_interval_denominator: 0, // Invalid denominator 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/validation.rs b/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_function/validation.rs index be07457a7fd..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 @@ -961,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 @@ -980,7 +980,9 @@ mod tests { decrease_per_interval_numerator: 1, decrease_per_interval_denominator: 2, 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); @@ -997,7 +999,9 @@ mod tests { decrease_per_interval_numerator: 1, decrease_per_interval_denominator: 2, 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); @@ -1014,7 +1018,9 @@ mod tests { decrease_per_interval_numerator: 1, decrease_per_interval_denominator: 0, 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); From 40a03c837bfd90c980b52dfd2a425cea5985b6ac Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Tue, 15 Apr 2025 21:23:48 +0700 Subject: [PATCH 28/28] fmt --- .../distribution_function/evaluate.rs | 3 +- .../distribution/perpetual/block_based.rs | 136 +++++++++--------- 2 files changed, 70 insertions(+), 69 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 b67586f6259..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 @@ -82,7 +82,8 @@ impl DistributionFunction { 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); + let reduction_numerator = + denominator.saturating_sub(*decrease_per_interval_numerator as u64); for _ in 0..steps_passed { numerator = numerator * reduction_numerator / denominator; 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 be12d7dfb51..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 @@ -77,7 +77,7 @@ mod perpetual_distribution_block { None, None, ) - .expect("expect to create documents batch transition"); + .expect("expect to create documents batch transition"); let claim_serialized_transition = claim_transition .serialize_to_bytes() @@ -145,7 +145,7 @@ mod perpetual_distribution_block { None, None, ) - .expect("expect to create documents batch transition"); + .expect("expect to create documents batch transition"); let claim_serialized_transition = claim_transition .serialize_to_bytes() @@ -215,7 +215,7 @@ mod perpetual_distribution_block { None, None, ) - .expect("expect to create documents batch transition"); + .expect("expect to create documents batch transition"); let claim_serialized_transition = claim_transition .serialize_to_bytes() @@ -326,7 +326,7 @@ mod perpetual_distribution_block { None, None, ) - .expect("expect to create documents batch transition"); + .expect("expect to create documents batch transition"); let claim_serialized_transition = claim_transition .serialize_to_bytes() @@ -448,7 +448,7 @@ mod perpetual_distribution_block { None, None, ) - .expect("expect to create documents batch transition"); + .expect("expect to create documents batch transition"); let claim_serialized_transition = claim_transition .serialize_to_bytes() @@ -546,7 +546,7 @@ mod fixed_amount { 10, None, ) - .expect("\n-> fixed amount should pass"); + .expect("\n-> fixed amount should pass"); } /// Test case for overflow error. @@ -582,7 +582,7 @@ mod fixed_amount { 10, None, ) - .expect("\n-> fixed amount should pass"); + .expect("\n-> fixed amount should pass"); } #[test] @@ -597,7 +597,7 @@ mod fixed_amount { 10, None, ) - .expect_err("\namount should not be 0\n"); + .expect_err("\namount should not be 0\n"); } #[test] @@ -625,7 +625,7 @@ mod fixed_amount { 10, Some(Some(200_000)), ) - .expect("\nfixed amount zero increase\n"); + .expect("\nfixed amount zero increase\n"); } /// Given a fixed amount distribution with value of u64::MAX, @@ -640,7 +640,7 @@ mod fixed_amount { 10, None, ) - .expect_err("u64::Max is too much for DistributionFunction::FixedAmount"); + .expect_err("u64::Max is too much for DistributionFunction::FixedAmount"); } /// Given a fixed amount distribution with value of u64::MAX, @@ -661,7 +661,7 @@ mod fixed_amount { 10, None, ) - .expect("MAX_DISTRIBUTION_PARAM should be valid DistributionFunction::FixedAmount"); + .expect("MAX_DISTRIBUTION_PARAM should be valid DistributionFunction::FixedAmount"); } } mod random { @@ -733,7 +733,7 @@ mod random { 10, None, ) - .expect("no rewards"); + .expect("no rewards"); } #[test] fn test_block_based_perpetual_random_0_u64_max_should_error_at_validation() { @@ -747,7 +747,7 @@ mod random { 10, None, ) - .expect_err("max is too much for DistributionFunction::Random"); + .expect_err("max is too much for DistributionFunction::Random"); } #[test] @@ -766,7 +766,7 @@ mod random { 10, None, ) - .expect("no rewards"); + .expect("no rewards"); } /// Given a random distribution function with min=10, max=30, @@ -809,9 +809,9 @@ mod random { ))); }), ) - .with_step_success_fn(move |balance: u64| { - balances.lock().unwrap().push(balance); - }); + .with_step_success_fn(move |balance: u64| { + balances.lock().unwrap().push(balance); + }); suite.execute(&tests).expect("should execute"); @@ -990,7 +990,7 @@ mod step_decreasing { INITIAL_BALANCE + 9_900 + 9_801 + 9_702 + 9_604, ], ) - .expect("expected to succeed"); + .expect("expected to succeed"); } #[test] @@ -1023,7 +1023,7 @@ mod step_decreasing { + 8_946, ], ) - .expect("expected to succeed"); + .expect("expected to succeed"); } #[test] @@ -1041,7 +1041,7 @@ mod step_decreasing { 1, vec![], ) - .expect_err("should not allow to increase"); + .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}" @@ -1063,7 +1063,7 @@ mod step_decreasing { 1, vec![], ) - .expect_err("should not allow to increase"); + .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}" @@ -1086,7 +1086,7 @@ mod step_decreasing { 1, sum_till_for_100k_step_1_interval_1(steps), ) - .expect("should pass"); + .expect("should pass"); } #[test] @@ -1114,7 +1114,7 @@ mod step_decreasing { 1, expected_amounts, ) - .expect("should pass"); + .expect("should pass"); } #[test] @@ -1143,7 +1143,7 @@ mod step_decreasing { 1, expected_amounts, ) - .expect("should pass"); + .expect("should pass"); } #[test] @@ -1173,7 +1173,7 @@ mod step_decreasing { 1, expected_amounts, ) - .expect("should pass"); + .expect("should pass"); } #[test] @@ -1200,7 +1200,7 @@ mod step_decreasing { 1, expected_amounts, ) - .expect("should pass"); + .expect("should pass"); } #[test] @@ -1221,8 +1221,8 @@ mod step_decreasing { 1, expected_amounts.clone(), ) - .map_err(|e| format!("failed with min {}: {}", min, e)) - .expect("should pass"); + .map_err(|e| format!("failed with min {}: {}", min, e)) + .expect("should pass"); } } @@ -1250,8 +1250,8 @@ mod step_decreasing { 1, expected_amounts, ) - .map_err(|e| format!("failed with min {}: {}", min, e)) - .expect("should pass"); + .map_err(|e| format!("failed with min {}: {}", min, e)) + .expect("should pass"); } } @@ -1270,7 +1270,7 @@ mod step_decreasing { 1, vec![], ) - .expect_err("should fail"); + .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}" @@ -1295,7 +1295,7 @@ mod step_decreasing { MAX_DISTRIBUTION_PARAM * 10 + INITIAL_BALANCE, ], ) - .expect("should succeed"); + .expect("should succeed"); } #[test] @@ -1323,7 +1323,7 @@ mod step_decreasing { 1, expected_balances, ) - .expect("should succeed"); + .expect("should succeed"); } #[test] @@ -1341,7 +1341,7 @@ mod step_decreasing { 1, vec![], ) - .expect_err("should fail"); + .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}" @@ -1368,7 +1368,7 @@ mod step_decreasing { distribution_interval, expected_balances, ) - .expect("should pass"); + .expect("should pass"); } #[test] @@ -1391,7 +1391,7 @@ mod step_decreasing { distribution_interval, expected_balances, ) - .expect("should pass"); + .expect("should pass"); } #[test] @@ -1415,7 +1415,7 @@ mod step_decreasing { distribution_interval, expected_balances, ) - .expect("should pass"); + .expect("should pass"); } #[test] @@ -1453,7 +1453,7 @@ mod step_decreasing { distribution_interval, expected_balances, ) - .expect("should pass"); + .expect("should pass"); } /// Test various combinations of [DistributionFunction::StepDecreasingAmount] distribution. @@ -1508,9 +1508,9 @@ mod step_decreasing { distribution_interval, None, ) - .inspect_err(|e| { - tracing::error!(e); - }) + .inspect_err(|e| { + tracing::error!(e); + }) } } @@ -1563,10 +1563,10 @@ mod stepwise { distribution_interval, None, ) - .inspect_err(|e| { - tracing::error!("{}", e); - }) - .expect("stepwise should pass"); + .inspect_err(|e| { + tracing::error!("{}", e); + }) + .expect("stepwise should pass"); } } @@ -1709,9 +1709,9 @@ mod linear { distribution_interval, None, ) - .inspect_err(|e| { - tracing::error!("{}", e); - }) + .inspect_err(|e| { + tracing::error!("{}", e); + }) } } @@ -1912,7 +1912,7 @@ mod polynomial { &[(1, 100_000, false), (2, 100_001, true)], 1, ) - .expect("should panic"); + .expect("should panic"); unreachable!("should panic"); } #[test] @@ -1974,9 +1974,9 @@ mod polynomial { distribution_interval, None, ) - .inspect_err(|e| { - tracing::error!("{}", e); - }) + .inspect_err(|e| { + tracing::error!("{}", e); + }) } /// Test various combinations of `m/n` in [DistributionFunction::Polynomial] distribution. @@ -2008,12 +2008,12 @@ mod polynomial { .set_perpetual_distribution(Some(TokenPerpetualDistribution::V0( TokenPerpetualDistributionV0 { distribution_type: - RewardDistributionType::BlockBasedDistribution { - interval: 1, - function: dist, - }, + RewardDistributionType::BlockBasedDistribution { + interval: 1, + function: dist, + }, distribution_recipient: - TokenDistributionRecipient::ContractOwner, + TokenDistributionRecipient::ContractOwner, }, ))); }), @@ -2346,9 +2346,9 @@ mod logarithmic { distribution_interval, None, ) - .inspect_err(|e| { - tracing::error!("{}", e); - }) + .inspect_err(|e| { + tracing::error!("{}", e); + }) } } @@ -2631,9 +2631,9 @@ mod inverted_logarithmic { distribution_interval, None, ) - .inspect_err(|e| { - tracing::error!("{}", e); - }) + .inspect_err(|e| { + tracing::error!("{}", e); + }) } } @@ -2793,7 +2793,7 @@ mod test_suite { token_configuration_modification: None, // setup later on_step_success: Box::new(|_| {}), } - .with_genesis(1, genesis_time_ms); + .with_genesis(1, genesis_time_ms); if let Some(token_configuration_modification) = token_configuration_modification { me.with_token_configuration_modification_fn(token_configuration_modification) @@ -2806,9 +2806,9 @@ mod test_suite { pub(crate) fn with_token_configuration_modification_fn( mut self, token_configuration_modification: impl FnOnce(&mut TokenConfiguration) - + Send - + Sync - + 'static, + + Send + + Sync + + 'static, ) -> Self { if let Some(previous) = self.token_configuration_modification.take() { let f = Box::new(move |token_configuration: &mut TokenConfiguration| { @@ -2934,7 +2934,7 @@ mod test_suite { None, None, ) - .expect("expect to create documents batch transition"); + .expect("expect to create documents batch transition"); let claim_serialized_transition = claim_transition .serialize_to_bytes()