diff --git a/Cargo.lock b/Cargo.lock index 8ad8efa2be1..124cf12fe1a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2142,6 +2142,7 @@ dependencies = [ "key-wallet", "key-wallet-manager", "lazy_static", + "libm", "log", "nohash-hasher", "num_enum 0.7.6", diff --git a/packages/rs-dpp/Cargo.toml b/packages/rs-dpp/Cargo.toml index a0f6f0b6889..02f38fd065c 100644 --- a/packages/rs-dpp/Cargo.toml +++ b/packages/rs-dpp/Cargo.toml @@ -46,6 +46,7 @@ jsonschema = { git = "https://github.com/dashpay/jsonschema-rs", branch = "confi "draft202012", ], optional = true } lazy_static = { version = "1.4" } +libm = "=0.2.16" # exact: v1 reward math must be bit-identical on every node num_enum = "0.7" bincode = { version = "=2.0.1", features = ["serde"] } rand = { version = "0.8.5", features = ["small_rng"] } 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 9b7326781b0..af2968d7f39 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 @@ -3,7 +3,63 @@ use crate::data_contract::associated_token::token_perpetual_distribution::distri DistributionFunction, DEFAULT_STEP_DECREASING_AMOUNT_MAX_CYCLES_BEFORE_TRAILING_DISTRIBUTION, MAX_DISTRIBUTION_PARAM, }; +use crate::version::FeatureVersion; use crate::ProtocolError; +use platform_version::version::PlatformVersion; + +/// Transcendental float operations used by the Polynomial, Exponential, Logarithmic and +/// InvertedLogarithmic distribution functions, selected once per +/// `distribution_function_evaluate_version`. +struct FloatOps { + pow: fn(f64, f64) -> f64, + exp: fn(f64) -> f64, + ln: fn(f64) -> f64, +} + +/// `distribution_function_evaluate_version` values `evaluate()` knows how to run. +const KNOWN_EVALUATE_VERSIONS: [FeatureVersion; 2] = [0, 1]; + +/// Rejects a `distribution_function_evaluate_version` that `evaluate()` cannot run. +/// +/// Called at the entry of every evaluation path, including the interval methods +/// whose fast paths (`FixedAmount`, empty intervals) never reach `evaluate()`, so an +/// unknown version fails the same way regardless of distribution type or bounds. +pub(super) fn check_evaluate_version( + platform_version: &PlatformVersion, +) -> Result { + let version = platform_version + .dpp + .token_versions + .distribution_function_evaluate_version; + if KNOWN_EVALUATE_VERSIONS.contains(&version) { + Ok(version) + } else { + Err(ProtocolError::UnknownVersionMismatch { + method: "DistributionFunction::evaluate".to_string(), + known_versions: KNOWN_EVALUATE_VERSIONS.to_vec(), + received: version, + }) + } +} + +impl FloatOps { + /// v0: std `f64` methods (platform-dependent results). + /// v1: `libm` (bit-identical results on every platform). + fn for_version(platform_version: &PlatformVersion) -> Result { + match check_evaluate_version(platform_version)? { + 0 => Ok(FloatOps { + pow: f64::powf, + exp: f64::exp, + ln: f64::ln, + }), + _ => Ok(FloatOps { + pow: libm::pow, + exp: libm::exp, + ln: libm::log, + }), + } + } +} impl DistributionFunction { /// Evaluates the distribution function at the given period `x`. @@ -19,7 +75,11 @@ impl DistributionFunction { &self, contract_registration_step: u64, x: u64, + platform_version: &PlatformVersion, ) -> Result { + // Resolved up front so an unknown version is rejected uniformly for every variant, + // including the integer-only ones that never call into it. + let float_ops = FloatOps::for_version(platform_version)?; match self { DistributionFunction::FixedAmount { amount: n } => { // For fixed amount, simply return n. @@ -222,7 +282,7 @@ impl DistributionFunction { )); } - let diff_exp = (diff as f64).powf(exponent); + let diff_exp = (float_ops.pow)(diff as f64, exponent); if !diff_exp.is_finite() { return if diff_exp.is_sign_positive() { @@ -326,7 +386,8 @@ impl DistributionFunction { } let exponent = (*m as f64) * (diff as f64) / (*n as f64); - let value = ((*a as f64) * exponent.exp() / (*d as f64)) + (*b as f64); + let exp_val = (float_ops.exp)(exponent); + let value = ((*a as f64) * exp_val / (*d as f64)) + (*b as f64); if let Some(max_value) = max_value { if value.is_infinite() && value.is_sign_positive() || value > *max_value as f64 { @@ -400,13 +461,11 @@ impl DistributionFunction { (*m as f64) * (diff as f64) / (*n as f64) }; - let log_val = argument.ln(); + let log_val = (float_ops.ln)(argument); // Ensure the computed value is finite and within the u64 range. if !log_val.is_finite() || log_val > (u64::MAX as f64) { - return Err(ProtocolError::Overflow( - "InvertedLogarithmic: evaluation overflow", - )); + return Err(ProtocolError::Overflow("Logarithmic: evaluation overflow")); } let intermediate = if *a == 1 { @@ -424,12 +483,12 @@ impl DistributionFunction { *max_value as i64 } else { return Err(ProtocolError::Overflow( - "InvertedLogarithmic: evaluation overflow intermediate bigger than i64::max", + "Logarithmic: evaluation overflow intermediate bigger than i64::max", )); } } else { return Err(ProtocolError::Overflow( - "InvertedLogarithmic: evaluation overflow intermediate bigger than i64::max", + "Logarithmic: evaluation overflow intermediate bigger than i64::max", )); } } else { @@ -437,20 +496,20 @@ impl DistributionFunction { .checked_add(*b as i64) .or(max_value.map(|max| max as i64)) .ok_or(ProtocolError::Overflow( - "InvertedLogarithmic: evaluation overflow when adding b", + "Logarithmic: evaluation overflow when adding b", ))? } } else { if !intermediate.is_finite() || intermediate > (i64::MAX as f64) { return Err(ProtocolError::Overflow( - "InvertedLogarithmic: evaluation overflow intermediate bigger than i64::max", + "Logarithmic: evaluation overflow intermediate bigger than i64::max", )); } ((intermediate / (*d as f64)).floor() as i64) .checked_add(*b as i64) .or(max_value.map(|max| max as i64)) .ok_or(ProtocolError::Overflow( - "InvertedLogarithmic: evaluation overflow when adding b", + "Logarithmic: evaluation overflow when adding b", ))? }; @@ -538,7 +597,7 @@ impl DistributionFunction { )); } - let log_val = argument.ln(); + let log_val = (float_ops.ln)(argument); // Ensure the computed value is finite and within the u64 range. if !log_val.is_finite() || log_val > (u64::MAX as f64) { @@ -606,14 +665,146 @@ impl DistributionFunction { #[cfg(test)] mod tests { use super::*; + use platform_version::version::PlatformVersion; use std::collections::BTreeMap; + use std::sync::LazyLock; + + /// `PlatformVersion::latest()` with `distribution_function_evaluate_version` forced to + /// `version`, so a test's expectations do not silently move when `latest()` does. + fn evaluate_version(version: FeatureVersion) -> PlatformVersion { + let mut platform_version = PlatformVersion::latest().clone(); + platform_version + .dpp + .token_versions + .distribution_function_evaluate_version = version; + platform_version + } + + /// Evaluation version 0: std `f64` transcendental methods (platform-dependent). + fn v0() -> &'static PlatformVersion { + static V0: LazyLock = LazyLock::new(|| evaluate_version(0)); + &V0 + } + + /// Evaluation version 1: `libm` (bit-identical on every platform). + fn v1() -> &'static PlatformVersion { + static V1: LazyLock = LazyLock::new(|| evaluate_version(1)); + &V1 + } + + #[test] + fn unknown_evaluate_version_is_rejected_for_every_variant() { + let unknown = evaluate_version(2); + let variants = [ + DistributionFunction::FixedAmount { amount: 100 }, + DistributionFunction::Random { min: 10, max: 100 }, + DistributionFunction::StepDecreasingAmount { + step_count: 10, + decrease_per_interval_numerator: 1, + decrease_per_interval_denominator: 2, + start_decreasing_offset: None, + max_interval_count: None, + distribution_start_amount: 100, + trailing_distribution_interval_amount: 0, + min_value: None, + }, + DistributionFunction::Stepwise(BTreeMap::from([(0, 100)])), + DistributionFunction::Linear { + a: 1, + d: 1, + start_step: None, + starting_amount: 50, + min_value: None, + max_value: None, + }, + DistributionFunction::Polynomial { + a: 1, + d: 1, + m: 2, + n: 1, + o: 0, + start_moment: None, + b: 0, + min_value: None, + max_value: None, + }, + DistributionFunction::Exponential { + a: 1, + d: 1, + m: 1, + n: 1, + o: 0, + start_moment: None, + b: 0, + min_value: None, + max_value: None, + }, + DistributionFunction::Logarithmic { + a: 1, + d: 1, + m: 1, + n: 1, + o: 1, + start_moment: None, + b: 0, + min_value: None, + max_value: None, + }, + DistributionFunction::InvertedLogarithmic { + a: 1, + d: 1, + m: 1, + n: 100, + o: 1, + start_moment: None, + b: 0, + min_value: None, + max_value: None, + }, + ]; + + // Compile error when a variant is added: extend `variants` above. + for distribution in &variants { + match distribution { + DistributionFunction::FixedAmount { .. } + | DistributionFunction::Random { .. } + | DistributionFunction::StepDecreasingAmount { .. } + | DistributionFunction::Stepwise(_) + | DistributionFunction::Linear { .. } + | DistributionFunction::Polynomial { .. } + | DistributionFunction::Exponential { .. } + | DistributionFunction::Logarithmic { .. } + | DistributionFunction::InvertedLogarithmic { .. } => {} + } + } + + for distribution in variants { + assert!( + matches!( + distribution.evaluate(0, 5, &unknown), + Err(ProtocolError::UnknownVersionMismatch { + ref known_versions, + received: 2, + .. + }) if *known_versions == KNOWN_EVALUATE_VERSIONS + ), + "{distribution:?} must fail closed on evaluate version 2", + ); + distribution + .evaluate(0, 5, v0()) + .expect("version 0 must evaluate"); + distribution + .evaluate(0, 5, v1()) + .expect("version 1 must evaluate"); + } + } #[test] fn test_fixed_amount() { let distribution = DistributionFunction::FixedAmount { amount: 100 }; - assert_eq!(distribution.evaluate(0, 0).unwrap(), 100); - assert_eq!(distribution.evaluate(0, 50).unwrap(), 100); - assert_eq!(distribution.evaluate(0, 1000).unwrap(), 100); + assert_eq!(distribution.evaluate(0, 0, v0()).unwrap(), 100); + assert_eq!(distribution.evaluate(0, 50, v0()).unwrap(), 100); + assert_eq!(distribution.evaluate(0, 1000, v0()).unwrap(), 100); } #[test] @@ -624,12 +815,12 @@ mod tests { steps.insert(20, 25); let distribution = DistributionFunction::Stepwise(steps); - assert_eq!(distribution.evaluate(0, 0).unwrap(), 100); - assert_eq!(distribution.evaluate(0, 5).unwrap(), 100); - assert_eq!(distribution.evaluate(0, 10).unwrap(), 50); - assert_eq!(distribution.evaluate(0, 15).unwrap(), 50); - assert_eq!(distribution.evaluate(0, 20).unwrap(), 25); - assert_eq!(distribution.evaluate(0, 30).unwrap(), 25); + assert_eq!(distribution.evaluate(0, 0, v0()).unwrap(), 100); + assert_eq!(distribution.evaluate(0, 5, v0()).unwrap(), 100); + assert_eq!(distribution.evaluate(0, 10, v0()).unwrap(), 50); + assert_eq!(distribution.evaluate(0, 15, v0()).unwrap(), 50); + assert_eq!(distribution.evaluate(0, 20, v0()).unwrap(), 25); + assert_eq!(distribution.evaluate(0, 30, v0()).unwrap(), 25); } #[test] @@ -645,12 +836,12 @@ mod tests { min_value: Some(10), }; - assert_eq!(distribution.evaluate(0, 0).unwrap(), 100); - assert_eq!(distribution.evaluate(0, 9).unwrap(), 100); - assert_eq!(distribution.evaluate(0, 10).unwrap(), 50); - assert_eq!(distribution.evaluate(0, 20).unwrap(), 25); - assert_eq!(distribution.evaluate(0, 30).unwrap(), 12); - assert_eq!(distribution.evaluate(0, 40).unwrap(), 10); // Should not go below min_value + assert_eq!(distribution.evaluate(0, 0, v0()).unwrap(), 100); + assert_eq!(distribution.evaluate(0, 9, v0()).unwrap(), 100); + assert_eq!(distribution.evaluate(0, 10, v0()).unwrap(), 50); + assert_eq!(distribution.evaluate(0, 20, v0()).unwrap(), 25); + assert_eq!(distribution.evaluate(0, 30, v0()).unwrap(), 12); + assert_eq!(distribution.evaluate(0, 40, v0()).unwrap(), 10); // Should not go below min_value } #[test] @@ -667,7 +858,7 @@ mod tests { }; assert!(matches!( - distribution.evaluate(0, 10), + distribution.evaluate(0, 10, v0()), Err(ProtocolError::DivideByZero(_)) )); } @@ -679,7 +870,7 @@ mod tests { let distribution = DistributionFunction::Random { min: 10, max: 100 }; for x in 0..100 { - let result = distribution.evaluate(0, x).unwrap(); + let result = distribution.evaluate(0, x, v0()).unwrap(); assert!( (10..=100).contains(&result), "Random value {} is out of range for x = {}", @@ -694,7 +885,7 @@ mod tests { let distribution = DistributionFunction::Random { min: 42, max: 42 }; for x in 0..10 { - let result = distribution.evaluate(0, x).unwrap(); + let result = distribution.evaluate(0, x, v0()).unwrap(); assert_eq!( result, 42, "Expected fixed output 42, got {} for x = {}", @@ -707,7 +898,7 @@ mod tests { fn test_random_distribution_invalid_range() { let distribution = DistributionFunction::Random { min: 50, max: 40 }; - let result = distribution.evaluate(0, 0); + let result = distribution.evaluate(0, 0, v0()); assert!( matches!(result, Err(ProtocolError::Overflow(_))), "Expected ProtocolError::Overflow but got {:?}", @@ -719,8 +910,8 @@ mod tests { fn test_random_distribution_deterministic_for_same_x() { let distribution = DistributionFunction::Random { min: 10, max: 100 }; - let value1 = distribution.evaluate(0, 42).unwrap(); - let value2 = distribution.evaluate(0, 42).unwrap(); + let value1 = distribution.evaluate(0, 42, v0()).unwrap(); + let value2 = distribution.evaluate(0, 42, v0()).unwrap(); assert_eq!( value1, value2, @@ -732,8 +923,8 @@ mod tests { fn test_random_distribution_varies_for_different_x() { let distribution = DistributionFunction::Random { min: 10, max: 100 }; - let value1 = distribution.evaluate(0, 1).unwrap(); - let value2 = distribution.evaluate(0, 2).unwrap(); + let value1 = distribution.evaluate(0, 1, v0()).unwrap(); + let value2 = distribution.evaluate(0, 2, v0()).unwrap(); assert_ne!( value1, value2, @@ -754,10 +945,10 @@ mod tests { max_value: None, }; - assert_eq!(distribution.evaluate(0, 0).unwrap(), 50); - assert_eq!(distribution.evaluate(0, 2).unwrap(), 60); - assert_eq!(distribution.evaluate(0, 4).unwrap(), 70); - assert_eq!(distribution.evaluate(0, 6).unwrap(), 80); + assert_eq!(distribution.evaluate(0, 0, v0()).unwrap(), 50); + assert_eq!(distribution.evaluate(0, 2, v0()).unwrap(), 60); + assert_eq!(distribution.evaluate(0, 4, v0()).unwrap(), 70); + assert_eq!(distribution.evaluate(0, 6, v0()).unwrap(), 80); } #[test] @@ -771,9 +962,9 @@ mod tests { max_value: None, }; - assert_eq!(distribution.evaluate(0, 0).unwrap(), 100); - assert_eq!(distribution.evaluate(0, 10).unwrap(), 50); - assert_eq!(distribution.evaluate(0, 20).unwrap(), 10); // Should not go below min_value + assert_eq!(distribution.evaluate(0, 0, v0()).unwrap(), 100); + assert_eq!(distribution.evaluate(0, 10, v0()).unwrap(), 50); + assert_eq!(distribution.evaluate(0, 20, v0()).unwrap(), 10); // Should not go below min_value } #[test] @@ -788,7 +979,7 @@ mod tests { }; assert!(matches!( - distribution.evaluate(0, 10), + distribution.evaluate(0, 10, v0()), Err(ProtocolError::DivideByZero(_)) )); } @@ -810,10 +1001,10 @@ mod tests { max_value: None, }; - assert_eq!(distribution.evaluate(0, 0).unwrap(), 0); - assert_eq!(distribution.evaluate(0, 2).unwrap(), 18); - assert_eq!(distribution.evaluate(0, 3).unwrap(), 28); - assert_eq!(distribution.evaluate(0, 4).unwrap(), 42); + assert_eq!(distribution.evaluate(0, 0, v0()).unwrap(), 0); + assert_eq!(distribution.evaluate(0, 2, v0()).unwrap(), 18); + assert_eq!(distribution.evaluate(0, 3, v0()).unwrap(), 28); + assert_eq!(distribution.evaluate(0, 4, v0()).unwrap(), 42); } #[test] @@ -830,7 +1021,9 @@ mod tests { max_value: None, }; - let result = distribution.evaluate(0, 100000).expect("expected value"); + let result = distribution + .evaluate(0, 100000, v0()) + .expect("expected value"); assert_eq!(result, MAX_DISTRIBUTION_PARAM); } @@ -849,7 +1042,43 @@ mod tests { max_value: None, }; // (4 - 0 + 0)^(3/2) = 4^(3/2) = (sqrt(4))^3 = 2^3 = 8. - assert_eq!(distribution.evaluate(0, 4).unwrap(), 8); + assert_eq!(distribution.evaluate(0, 4, v0()).unwrap(), 8); + } + + #[test] + fn test_polynomial_fractional_power_rounding_boundary_is_deterministic() { + let distribution = DistributionFunction::Polynomial { + a: 1, + d: 1, + m: 1, + n: 3, + o: 0, + start_moment: Some(0), + b: 0, + min_value: None, + max_value: None, + }; + + // This pins the bit-exact output of the v1 (libm 0.2.16) path on a fixture that + // sits one ulp from flipping, so any change to the v1 math shows up as a + // consensus-relevant `4 != 5` failure here rather than on the network. + // + // The exponent `1.0 / 3.0` rounds *below* 1/3, so the exact value of + // `125^exponent` is 4.99999999999999955...; a correctly-rounded pow (glibc, + // Apple libm) returns 4.999999999999999, which truncates to 4. libm 0.2.16's + // pow returns exactly 5.0. Neither answer is "more right" for consensus: the + // point is that every node computes the same one. Do not "fix" this + // assertion if it starts failing after a libm bump -- that bump is a + // consensus change and needs a new evaluation version. + assert_eq!(distribution.evaluate(0, 125, v1()).unwrap(), 5); + + // The v0 result is platform-dependent by construction, so it is only + // sanity-checked to land on one of the two possible truncations. + let v0_result = distribution.evaluate(0, 125, v0()).unwrap(); + assert!( + v0_result == 4 || v0_result == 5, + "std powf(125, 1/3) truncated to {v0_result}" + ); } // Test: Negative coefficient a (should flip the sign) @@ -867,7 +1096,7 @@ mod tests { max_value: None, }; // f(x) = -1 * (x^2). For x = 3: -1 * (3^2) = -9. - assert_eq!(distribution.evaluate(0, 3).unwrap(), 0); + assert_eq!(distribution.evaluate(0, 3, v0()).unwrap(), 0); } // Test: Non-zero shift parameter s (shifting the x coordinate) @@ -885,9 +1114,9 @@ mod tests { max_value: None, }; // since it starts at 2 (that's like the contract registration at 2, so we should get 0 - assert_eq!(distribution.evaluate(0, 2).unwrap(), 0); + assert_eq!(distribution.evaluate(0, 2, v0()).unwrap(), 0); // At x = 3: (3 - 2)^2 = 1, f(3) = 2*1 + 10 = 12. - assert_eq!(distribution.evaluate(0, 3).unwrap(), 12); + assert_eq!(distribution.evaluate(0, 3, v0()).unwrap(), 12); } // Test: Non-zero offset o (shifting the base of the power) @@ -906,7 +1135,7 @@ mod tests { }; // f(x) = 2 * ((x - 0 + 3)^2) + 10. // At x = 1: (1 + 3) = 4, 4^2 = 16, then 2*16 + 10 = 42. - assert_eq!(distribution.evaluate(0, 1).unwrap(), 42); + assert_eq!(distribution.evaluate(0, 1, v0()).unwrap(), 42); } // Test: Linear function when exponent is 1 (m = 1, n = 1) @@ -924,7 +1153,7 @@ mod tests { max_value: None, }; // f(x) = 3*x + 5. At x = 10, f(10) = 30 + 5 = 35. - assert_eq!(distribution.evaluate(0, 10).unwrap(), 35); + assert_eq!(distribution.evaluate(0, 10, v0()).unwrap(), 35); } // Test: Cubic function (m = 3, n = 1) @@ -942,7 +1171,7 @@ mod tests { max_value: None, }; // f(x) = x^3. At x = 4, f(4) = 64. - assert_eq!(distribution.evaluate(0, 4).unwrap(), 64); + assert_eq!(distribution.evaluate(0, 4, v0()).unwrap(), 64); } // Test: Combination of non-zero offset and shift @@ -961,7 +1190,7 @@ mod tests { }; // f(x) = ( (x - 1 + 2)^2 ). // At x = 3: (3 - 1 + 2) = 4, and 4^2 = 16. - assert_eq!(distribution.evaluate(0, 3).unwrap(), 16); + assert_eq!(distribution.evaluate(0, 3, v0()).unwrap(), 16); } } mod exp { @@ -980,8 +1209,8 @@ mod tests { max_value: None, }; - assert_eq!(distribution.evaluate(0, 0).unwrap(), 11); - assert!(distribution.evaluate(0, 10).unwrap() > 20); + assert_eq!(distribution.evaluate(0, 0, v0()).unwrap(), 11); + assert!(distribution.evaluate(0, 10, v0()).unwrap() > 20); } #[test] @@ -999,7 +1228,7 @@ mod tests { }; assert!(matches!( - distribution.evaluate(0, 10), + distribution.evaluate(0, 10, v0()), Err(ProtocolError::DivideByZero(_)) )); } @@ -1018,9 +1247,9 @@ mod tests { max_value: None, }; - assert_eq!(distribution.evaluate(0, 0).unwrap(), 7); - assert_eq!(distribution.evaluate(0, 5).unwrap(), 301); - assert_eq!(distribution.evaluate(0, 10).unwrap(), 44057); + assert_eq!(distribution.evaluate(0, 0, v0()).unwrap(), 7); + assert_eq!(distribution.evaluate(0, 5, v0()).unwrap(), 301); + assert_eq!(distribution.evaluate(0, 10, v0()).unwrap(), 44057); } #[test] @@ -1037,9 +1266,9 @@ mod tests { max_value: None, }; - assert_eq!(distribution.evaluate(0, 0).unwrap(), 0); - assert_eq!(distribution.evaluate(0, 50).unwrap(), 14); - assert_eq!(distribution.evaluate(0, 100).unwrap(), 2202); + assert_eq!(distribution.evaluate(0, 0, v0()).unwrap(), 0); + assert_eq!(distribution.evaluate(0, 50, v0()).unwrap(), 14); + assert_eq!(distribution.evaluate(0, 100, v0()).unwrap(), 2202); } #[test] @@ -1056,11 +1285,11 @@ mod tests { max_value: Some(100000000), }; - assert_eq!(distribution.evaluate(0, 0).unwrap(), 1); - assert_eq!(distribution.evaluate(0, 2).unwrap(), 2980); - assert_eq!(distribution.evaluate(0, 4).unwrap(), 8886110); - assert_eq!(distribution.evaluate(0, 10).unwrap(), 100000000); - assert_eq!(distribution.evaluate(0, 100000).unwrap(), 100000000); + assert_eq!(distribution.evaluate(0, 0, v0()).unwrap(), 1); + assert_eq!(distribution.evaluate(0, 2, v0()).unwrap(), 2980); + assert_eq!(distribution.evaluate(0, 4, v0()).unwrap(), 8886110); + assert_eq!(distribution.evaluate(0, 10, v0()).unwrap(), 100000000); + assert_eq!(distribution.evaluate(0, 100000, v0()).unwrap(), 100000000); } #[test] @@ -1077,9 +1306,9 @@ mod tests { max_value: None, }; - assert_eq!(distribution.evaluate(0, 0).unwrap(), 12); // f(0) = (2 * e^(-1 * (0 - 0 + 0) / 1)) / 1 + 10 - assert_eq!(distribution.evaluate(0, 5).unwrap(), 10); - assert_eq!(distribution.evaluate(0, 10000).unwrap(), 10); + assert_eq!(distribution.evaluate(0, 0, v0()).unwrap(), 12); // f(0) = (2 * e^(-1 * (0 - 0 + 0) / 1)) / 1 + 10 + assert_eq!(distribution.evaluate(0, 5, v0()).unwrap(), 10); + assert_eq!(distribution.evaluate(0, 10000, v0()).unwrap(), 10); } #[test] @@ -1096,9 +1325,9 @@ mod tests { max_value: None, }; - assert_eq!(distribution.evaluate(0, 0).unwrap(), 12); // f(0) = (2 * e^(-1 * (0 - 0 + 0) / 1)) / 1 + 10 - assert_eq!(distribution.evaluate(0, 5).unwrap(), 11); - assert_eq!(distribution.evaluate(0, 100).unwrap(), 11); + assert_eq!(distribution.evaluate(0, 0, v0()).unwrap(), 12); // f(0) = (2 * e^(-1 * (0 - 0 + 0) / 1)) / 1 + 10 + assert_eq!(distribution.evaluate(0, 5, v0()).unwrap(), 11); + assert_eq!(distribution.evaluate(0, 100, v0()).unwrap(), 11); } #[test] @@ -1116,12 +1345,12 @@ mod tests { }; assert_eq!( - distribution.evaluate(0, 0).unwrap(), + distribution.evaluate(0, 0, v0()).unwrap(), 11, "Function should start at the max value" ); assert_eq!( - distribution.evaluate(0, 5).unwrap(), + distribution.evaluate(0, 5, v0()).unwrap(), 11, "Function should be clamped at max value" ); @@ -1141,13 +1370,48 @@ mod tests { max_value: None, }; - let result = distribution.evaluate(0, 100000); + let result = distribution.evaluate(0, 100000, v0()); assert!( matches!(result, Err(ProtocolError::Overflow(_))), "Expected overflow but got {:?}", result ); } + + #[test] + fn test_exponential_deterministic_libm_path() { + let distribution = DistributionFunction::Exponential { + a: 1, + d: 1, + m: -20, + n: 1, + o: 0, + start_moment: Some(0), + b: 0, + min_value: None, + max_value: None, + }; + + // Verify the deterministic libm path produces a consistent result + let v1_result = distribution.evaluate(0, 2, v1()).unwrap(); + // e^(-40) is extremely small but nonzero; result should be 0 after truncation + assert_eq!(v1_result, 0); + + // A case with a larger result: e^(2) ≈ 7.389 + let distribution2 = DistributionFunction::Exponential { + a: 1, + d: 1, + m: 1, + n: 1, + o: 0, + start_moment: Some(0), + b: 0, + min_value: None, + max_value: None, + }; + let v1_result2 = distribution2.evaluate(0, 2, v1()).unwrap(); + assert_eq!(v1_result2, 7); + } } mod log { use super::*; @@ -1165,8 +1429,8 @@ mod tests { max_value: None, }; - assert_eq!(distribution.evaluate(0, 1).unwrap(), 5); - assert!(distribution.evaluate(0, 10).unwrap() > 5); + assert_eq!(distribution.evaluate(0, 1, v0()).unwrap(), 5); + assert!(distribution.evaluate(0, 10, v0()).unwrap() > 5); } #[test] @@ -1183,8 +1447,8 @@ mod tests { max_value: Some(20), // Maximum bound should be enforced }; - assert_eq!(distribution.evaluate(0, 1).unwrap(), 7); // Clamped to min_value - assert!(distribution.evaluate(0, 10).unwrap() <= 20); // Should not exceed max_value + assert_eq!(distribution.evaluate(0, 1, v0()).unwrap(), 7); // Clamped to min_value + assert!(distribution.evaluate(0, 10, v0()).unwrap() <= 20); // Should not exceed max_value } #[test] @@ -1202,7 +1466,7 @@ mod tests { }; assert!(matches!( - distribution.evaluate(0, 1), + distribution.evaluate(0, 1, v0()), Err(ProtocolError::Overflow(_)) )); } @@ -1221,7 +1485,7 @@ mod tests { max_value: None, }; - let result = distribution.evaluate(0, 100); + let result = distribution.evaluate(0, 100, v0()); assert!(result.is_ok()); assert!(result.unwrap() > 10); // Function should increase over time } @@ -1241,7 +1505,7 @@ mod tests { }; assert!(matches!( - distribution.evaluate(0, 10), + distribution.evaluate(0, 10, v0()), Err(ProtocolError::DivideByZero(_)) )); } @@ -1261,10 +1525,30 @@ mod tests { }; assert!(matches!( - distribution.evaluate(0, 10), + distribution.evaluate(0, 10, v0()), Err(ProtocolError::DivideByZero(_)) )); } + + #[test] + fn test_logarithmic_deterministic_libm_path() { + // f(x) = 10 * ln(x) / 1 + 0, evaluate at x=100 + // ln(100) ≈ 4.605, * 10 = 46.05, truncated to 46 + let distribution = DistributionFunction::Logarithmic { + a: 10, + d: 1, + m: 1, + n: 1, + o: 1, + start_moment: Some(0), + b: 0, + min_value: None, + max_value: None, + }; + + let v1_result = distribution.evaluate(0, 100, v1()).unwrap(); + assert_eq!(v1_result, 46); + } } mod inverted_log { use super::*; @@ -1282,8 +1566,14 @@ mod tests { max_value: None, }; - assert!(distribution.evaluate(0, 1).unwrap() > distribution.evaluate(0, 5).unwrap()); - assert!(distribution.evaluate(0, 5).unwrap() > distribution.evaluate(0, 10).unwrap()); + assert!( + distribution.evaluate(0, 1, v0()).unwrap() + > distribution.evaluate(0, 5, v0()).unwrap() + ); + assert!( + distribution.evaluate(0, 5, v0()).unwrap() + > distribution.evaluate(0, 10, v0()).unwrap() + ); } #[test] @@ -1301,9 +1591,9 @@ mod tests { max_value: None, }; - let val1000 = distribution.evaluate(0, 1000).unwrap(); - let val2000 = distribution.evaluate(0, 2000).unwrap(); - let val3000 = distribution.evaluate(0, 3000).unwrap(); + let val1000 = distribution.evaluate(0, 1000, v0()).unwrap(); + let val2000 = distribution.evaluate(0, 2000, v0()).unwrap(); + let val3000 = distribution.evaluate(0, 3000, v0()).unwrap(); assert!(val1000 < val2000, "Function should be increasing"); assert!(val2000 < val3000, "Function should be increasing"); @@ -1323,7 +1613,7 @@ mod tests { max_value: None, }; - assert_eq!(distribution.evaluate(0, 1).unwrap(), 0); // Should be clamped to 0 + assert_eq!(distribution.evaluate(0, 1, v0()).unwrap(), 0); // Should be clamped to 0 } #[test] @@ -1340,7 +1630,7 @@ mod tests { max_value: None, }; - assert_eq!(distribution.evaluate(0, 1000).unwrap(), 7); // Should be clamped to min_value + assert_eq!(distribution.evaluate(0, 1000, v0()).unwrap(), 7); // Should be clamped to min_value } #[test] @@ -1358,7 +1648,7 @@ mod tests { max_value: Some(20), }; - assert_eq!(distribution.evaluate(0, 500).unwrap(), 20); // Should be clamped to max_value + assert_eq!(distribution.evaluate(0, 500, v0()).unwrap(), 20); // Should be clamped to max_value } #[test] @@ -1376,7 +1666,7 @@ mod tests { }; assert!(matches!( - distribution.evaluate(0, 1), + distribution.evaluate(0, 1, v0()), Err(ProtocolError::Overflow(_)) )); } @@ -1396,7 +1686,7 @@ mod tests { }; assert!(matches!( - distribution.evaluate(0, 10), + distribution.evaluate(0, 10, v0()), Err(ProtocolError::DivideByZero(_)) )); } @@ -1416,7 +1706,7 @@ mod tests { }; assert!(matches!( - distribution.evaluate(0, 10), + distribution.evaluate(0, 10, v0()), Err(ProtocolError::DivideByZero(_)) )); } @@ -1436,12 +1726,12 @@ mod tests { }; assert_eq!( - distribution.evaluate(0, 0).unwrap(), + distribution.evaluate(0, 0, v0()).unwrap(), 1, "Function should start at the max value" ); assert_eq!( - distribution.evaluate(0, 200).unwrap(), + distribution.evaluate(0, 200, v0()).unwrap(), 10, "Function should remain clamped at max value" ); @@ -1462,10 +1752,88 @@ mod tests { }; assert_eq!( - distribution.evaluate(0, 1000).unwrap(), + distribution.evaluate(0, 1000, v0()).unwrap(), 3, "Function should remain clamped at min value" ); } + + #[test] + fn test_inverted_logarithmic_deterministic_libm_path() { + // f(x) = 10 * ln(100 / (1 * x)) / 1 + 5 + // At x=0 (with start_moment=0 and o=1, so diff = 0 - 0 + 1 = 1, arg = 100/1 = 100): ln(100) ≈ 4.605, * 10 = 46.05 + 5 = 51 + let distribution = DistributionFunction::InvertedLogarithmic { + a: 10, + d: 1, + m: 1, + n: 100, + o: 1, + start_moment: Some(0), + b: 5, + min_value: None, + max_value: None, + }; + + let v1_result = distribution.evaluate(0, 0, v1()).unwrap(); + assert_eq!(v1_result, 51); + } + + /// The exact-value fixtures from the drive-abci `inverted_logarithmic` block-based + /// tests, pinned under v1 and then checked for agreement with v0, so a std/libm + /// divergence on a real distribution shape surfaces before activation rather + /// than as a chain split. The v0 values are not pinned: they are + /// platform-dependent by construction, and today agree on every platform CI runs. + #[test] + fn test_v0_and_v1_agree_on_block_based_fixtures() { + let fixtures: [(DistributionFunction, &[(u64, u64)]); 2] = [ + ( + DistributionFunction::InvertedLogarithmic { + a: 10000, + d: 1, + m: 1, + n: 5000, + o: 0, + start_moment: Some(0), + b: 0, + min_value: None, + max_value: None, + }, + &[ + (1, 85171), + (2, 78240), + (1000, 16094), + (4000, 2231), + (5000, 0), + (6000, 0), + ], + ), + ( + DistributionFunction::InvertedLogarithmic { + a: -2200, + d: 1, + m: 1, + n: 10000, + o: 3000, + start_moment: Some(0), + b: 4000, + min_value: None, + max_value: None, + }, + &[(1, 1351), (2, 1352), (1000, 1984), (4000, 3215)], + ), + ]; + + for (distribution, expectations) in fixtures { + for &(x, expected) in expectations { + let v1_value = distribution.evaluate(0, x, v1()).unwrap(); + assert_eq!(v1_value, expected, "v1 {distribution:?} at x={x}"); + assert_eq!( + distribution.evaluate(0, x, v0()).unwrap(), + v1_value, + "v0 disagrees with v1 for {distribution:?} at x={x}" + ); + } + } + } } } diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_function/evaluate_interval.rs b/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_function/evaluate_interval.rs index dd43374ad13..6bb535cefac 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_function/evaluate_interval.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_function/evaluate_interval.rs @@ -1,9 +1,9 @@ use std::ops::{Div, RangeInclusive}; -#[cfg(feature = "token-reward-explanations")] use platform_version::version::PlatformVersion; use crate::balances::credits::TokenAmount; use crate::block::epoch::EpochIndex; use crate::data_contract::associated_token::token_perpetual_distribution::distribution_function::DistributionFunction; +use crate::data_contract::associated_token::token_perpetual_distribution::distribution_function::evaluate::check_evaluate_version; #[cfg(feature = "token-reward-explanations")] use crate::data_contract::associated_token::token_perpetual_distribution::distribution_function::MAX_DISTRIBUTION_CYCLES_PARAM; use crate::data_contract::associated_token::token_perpetual_distribution::distribution_function::reward_ratio::RewardRatio; @@ -1571,10 +1571,15 @@ impl DistributionFunction { interval_end_included: RewardDistributionMoment, step: RewardDistributionMoment, get_epoch_reward_ratio: Option, + platform_version: &PlatformVersion, ) -> Result where F: Fn(RangeInclusive) -> Option, { + // Before any fast path: the FixedAmount and empty-interval returns below never + // reach evaluate(), and an unknown version must fail the same way for all of them. + check_evaluate_version(platform_version)?; + // Ensure moments are the same type. if !(interval_start_excluded.same_type(&step) && interval_start_excluded.same_type(&interval_end_included)) @@ -1649,8 +1654,11 @@ impl DistributionFunction { let mut current_point = first_step; while current_point <= last_step { - let base_amount = - self.evaluate(distribution_start_step.to_u64(), current_point.to_u64())?; + let base_amount = self.evaluate( + distribution_start_step.to_u64(), + current_point.to_u64(), + platform_version, + )?; let amount = if let ( RewardDistributionMoment::EpochBasedMoment(epoch_index), @@ -1707,6 +1715,7 @@ impl DistributionFunction { /// - `Ok(IntervalEvaluationExplanation)` containing the result and detailed explanation. /// - `Err(ProtocolError)` on mismatched types, zero steps, or overflow. #[cfg(feature = "token-reward-explanations")] + #[allow(clippy::too_many_arguments)] pub fn evaluate_interval_with_explanation( &self, distribution_start: RewardDistributionMoment, @@ -1715,10 +1724,14 @@ impl DistributionFunction { step: RewardDistributionMoment, get_epoch_reward_ratio: Option, is_first_claim: bool, + platform_version: &PlatformVersion, ) -> Result where F: Fn(RangeInclusive) -> Option, { + // Before any fast path, for the same reason as in evaluate_interval. + check_evaluate_version(platform_version)?; + let mut explanation = IntervalEvaluationExplanation { distribution_function: self.clone(), interval_start_excluded, @@ -1838,8 +1851,11 @@ impl DistributionFunction { let mut collected_ratios = Vec::new(); while current_point <= last_step { - let base_amount = - self.evaluate(distribution_start_step.to_u64(), current_point.to_u64())?; + let base_amount = self.evaluate( + distribution_start_step.to_u64(), + current_point.to_u64(), + platform_version, + )?; let (amount, reward_ratio) = if let ( RewardDistributionMoment::EpochBasedMoment(epoch_index), @@ -1896,6 +1912,69 @@ impl DistributionFunction { mod tests { use super::*; + /// Both interval entry points must reject an unknown evaluation version before the + /// FixedAmount and empty-interval fast paths, which never reach `evaluate()`. + #[test] + fn unknown_evaluate_version_is_rejected_before_interval_fast_paths() { + let mut unknown = PlatformVersion::latest().clone(); + unknown + .dpp + .token_versions + .distribution_function_evaluate_version = 2; + let no_ratio = None::) -> Option>; + let start = RewardDistributionMoment::BlockBasedMoment(0); + let step = RewardDistributionMoment::BlockBasedMoment(1); + let block = RewardDistributionMoment::BlockBasedMoment; + + // (distribution, interval_start_excluded, interval_end_included) + let cases = [ + // FixedAmount fast path, non-empty interval + ( + DistributionFunction::FixedAmount { amount: 10 }, + block(0), + block(5), + ), + // Empty interval fast path on a variant that would otherwise reach evaluate() + ( + DistributionFunction::Linear { + a: 1, + d: 1, + start_step: None, + starting_amount: 1, + min_value: None, + max_value: None, + }, + block(5), + block(5), + ), + ]; + + for (distribution, from, to) in cases { + let result = distribution.evaluate_interval(start, from, to, step, no_ratio, &unknown); + assert!( + matches!( + result, + Err(ProtocolError::UnknownVersionMismatch { received: 2, .. }) + ), + "evaluate_interval({distribution:?}, {from:?}..={to:?}) returned {result:?}" + ); + + #[cfg(feature = "token-reward-explanations")] + { + let result = distribution.evaluate_interval_with_explanation( + start, from, to, step, no_ratio, true, &unknown, + ); + assert!( + matches!( + result, + Err(ProtocolError::UnknownVersionMismatch { received: 2, .. }) + ), + "evaluate_interval_with_explanation({distribution:?}, {from:?}..={to:?}) returned {result:?}" + ); + } + } + } + mod epoch_tests { use super::*; @@ -1915,6 +1994,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -1946,6 +2026,7 @@ mod tests { step, None::) -> Option>, false, + PlatformVersion::latest(), ) .unwrap(); @@ -1976,6 +2057,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -2007,6 +2089,7 @@ mod tests { step, None::) -> Option>, false, + PlatformVersion::latest(), ) .unwrap(); @@ -2048,6 +2131,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -2086,6 +2170,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -2124,6 +2209,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -2165,6 +2251,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -2206,6 +2293,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -2247,6 +2335,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -2288,6 +2377,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -2318,6 +2408,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -2359,6 +2450,7 @@ mod tests { step, Some(get_ratio), true, + PlatformVersion::latest(), ) .unwrap(); @@ -2383,6 +2475,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -2415,6 +2508,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -2451,6 +2545,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -2476,6 +2571,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -2504,6 +2600,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -2542,6 +2639,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -2575,6 +2673,7 @@ mod tests { step, None::) -> Option>, false, + PlatformVersion::latest(), ) .unwrap(); @@ -2621,6 +2720,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -2651,6 +2751,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -2693,6 +2794,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -2718,6 +2820,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -2743,6 +2846,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -2772,6 +2876,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -2813,6 +2918,7 @@ mod tests { step, None::) -> Option>, false, + PlatformVersion::latest(), ) .unwrap(); @@ -2855,6 +2961,7 @@ mod tests { step, None::) -> Option>, false, + PlatformVersion::latest(), ) .unwrap(); @@ -2885,6 +2992,7 @@ mod tests { step, None::) -> Option>, false, + PlatformVersion::latest(), ) .unwrap(); @@ -2911,6 +3019,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -2942,6 +3051,7 @@ mod tests { step, None::) -> Option>, false, + PlatformVersion::latest(), ) .unwrap(); @@ -2980,6 +3090,7 @@ mod tests { step, None::) -> Option>, false, + PlatformVersion::latest(), ) .unwrap(); @@ -3018,6 +3129,7 @@ mod tests { step, None::) -> Option>, false, + PlatformVersion::latest(), ) .unwrap(); @@ -3059,6 +3171,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -3100,6 +3213,7 @@ mod tests { step, None::) -> Option>, false, + PlatformVersion::latest(), ) .unwrap(); @@ -3141,6 +3255,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -3182,6 +3297,7 @@ mod tests { step, None::) -> Option>, false, + PlatformVersion::latest(), ) .unwrap(); @@ -3223,6 +3339,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -3264,6 +3381,7 @@ mod tests { step, None::) -> Option>, false, + PlatformVersion::latest(), ) .unwrap(); @@ -3294,6 +3412,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -3334,6 +3453,7 @@ mod tests { step, Some(get_ratio), true, + PlatformVersion::latest(), ) .unwrap(); @@ -3358,6 +3478,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -3393,6 +3514,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -3431,6 +3553,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -3464,6 +3587,7 @@ mod tests { step, None::) -> Option>, false, + PlatformVersion::latest(), ) .unwrap(); @@ -3494,6 +3618,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -3535,6 +3660,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -3575,6 +3701,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -3607,6 +3734,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -3638,6 +3766,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -3671,6 +3800,7 @@ mod tests { step, None::) -> Option>, false, + PlatformVersion::latest(), ) .unwrap(); @@ -3714,6 +3844,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -3757,6 +3888,7 @@ mod tests { step, None::) -> Option>, false, + PlatformVersion::latest(), ) .unwrap(); @@ -3797,6 +3929,7 @@ mod tests { step, None::) -> Option>, false, + PlatformVersion::latest(), ) .unwrap(); @@ -3837,6 +3970,7 @@ mod tests { step, None::) -> Option>, false, + PlatformVersion::latest(), ) .unwrap(); @@ -3880,6 +4014,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -3928,6 +4063,7 @@ mod tests { step, None::) -> Option>, false, + PlatformVersion::latest(), ) .unwrap(); @@ -3976,6 +4112,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -4024,6 +4161,7 @@ mod tests { step, None::) -> Option>, false, + PlatformVersion::latest(), ) .unwrap(); @@ -4072,6 +4210,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -4120,6 +4259,7 @@ mod tests { step, None::) -> Option>, false, + PlatformVersion::latest(), ) .unwrap(); @@ -4168,6 +4308,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -4216,6 +4357,7 @@ mod tests { step, None::) -> Option>, false, + PlatformVersion::latest(), ) .unwrap(); @@ -4253,6 +4395,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -4290,6 +4433,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -4315,6 +4459,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -4340,6 +4485,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -4379,6 +4525,7 @@ mod tests { step, Some(get_ratio), true, + PlatformVersion::latest(), ) .unwrap(); @@ -4403,6 +4550,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -4435,6 +4583,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -4477,6 +4626,7 @@ mod tests { step, Some(get_ratio), false, + PlatformVersion::latest(), ) .unwrap(); @@ -4520,6 +4670,7 @@ mod tests { step, Some(get_ratio), true, // first claim + PlatformVersion::latest(), ) .unwrap(); @@ -4563,6 +4714,7 @@ mod tests { step, Some(get_ratio), false, + PlatformVersion::latest(), ) .unwrap(); @@ -4620,6 +4772,7 @@ mod tests { step, Some(get_ratio), false, + PlatformVersion::latest(), ) .unwrap(); @@ -4678,6 +4831,7 @@ mod tests { step, None::) -> Option>, false, + PlatformVersion::latest(), ) .unwrap(); 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 c9738c71f1f..3fb4a0a964b 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 @@ -266,7 +266,7 @@ impl DistributionFunction { min_value: *min_value, max_value: *max_value, } - .evaluate(0, start_moment)?; + .evaluate(0, start_moment, platform_version)?; if *a > 0 { // we want to put in the max value to see if we are starting off at the max @@ -444,7 +444,7 @@ impl DistributionFunction { min_value: *min_value, max_value: *max_value, } - .evaluate(0, start_moment)?; + .evaluate(0, start_moment, platform_version)?; // Now, based on the monotonicity implied by (*a) * (*m), // check for incoherence: @@ -634,7 +634,7 @@ impl DistributionFunction { min_value: *min_value, max_value: *max_value, } - .evaluate(0, start_moment)?; + .evaluate(0, start_moment, platform_version)?; if *m > 0 { // we want to put in the max value to see if we are starting off at the max @@ -812,7 +812,7 @@ impl DistributionFunction { min_value: *min_value, max_value: *max_value, } - .evaluate(0, start_moment)?; + .evaluate(0, start_moment, platform_version)?; if let Some(max) = max_value { if start_token_amount == *max { @@ -976,7 +976,7 @@ impl DistributionFunction { min_value: *min_value, max_value: *max_value, } - .evaluate(0, start_moment)?; + .evaluate(0, start_moment, platform_version)?; // Determine the function's monotonicity. // For InvertedLogarithmic, f'(x) = -a / (d * (x - s + o)). @@ -1822,7 +1822,7 @@ mod tests { min_value: Some(0), max_value: Some(100), }; - let eval_result = dist.evaluate(0, 4); + let eval_result = dist.evaluate(0, 4, PlatformVersion::latest()); assert_eq!( eval_result.unwrap(), 8, diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/reward_distribution_type/evaluate_interval.rs b/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/reward_distribution_type/evaluate_interval.rs index 7559a377950..1cd2fccbef3 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/reward_distribution_type/evaluate_interval.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/reward_distribution_type/evaluate_interval.rs @@ -7,6 +7,7 @@ use crate::data_contract::associated_token::token_perpetual_distribution::distri use crate::data_contract::associated_token::token_perpetual_distribution::reward_distribution_moment::RewardDistributionMoment; use crate::data_contract::associated_token::token_perpetual_distribution::reward_distribution_type::RewardDistributionType; use crate::ProtocolError; +use platform_version::version::PlatformVersion; impl RewardDistributionType { /// Computes the total rewards emitted in a given interval based on the provided distribution moments. @@ -36,6 +37,7 @@ impl RewardDistributionType { start_at_moment: RewardDistributionMoment, current_moment_included: RewardDistributionMoment, get_epoch_reward_ratio: Option, + platform_version: &PlatformVersion, ) -> Result where F: Fn(RangeInclusive) -> Option, @@ -46,6 +48,7 @@ impl RewardDistributionType { current_moment_included, self.interval(), get_epoch_reward_ratio, + platform_version, ) } @@ -80,6 +83,7 @@ impl RewardDistributionType { current_moment_included: RewardDistributionMoment, get_epoch_reward_ratio: Option, is_first_claim: bool, + platform_version: &PlatformVersion, ) -> Result where F: Fn(RangeInclusive) -> Option, @@ -91,6 +95,7 @@ impl RewardDistributionType { self.interval(), get_epoch_reward_ratio, is_first_claim, + platform_version, ) } } 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 c266e4c0c32..b294f16b3cf 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 @@ -2333,6 +2333,7 @@ mod logarithmic { mod inverted_logarithmic { use super::test_suite::check_heights; use dpp::data_contract::associated_token::token_perpetual_distribution::distribution_function::DistributionFunction::{self,InvertedLogarithmic}; + use platform_version::version::PlatformVersion; #[tokio::test] async fn inv_log_distribution_very_low_emission() -> Result<(), String> { @@ -2353,9 +2354,13 @@ mod inverted_logarithmic { (2, 100_001, false), (50000, 100_001, false), ]; - let x_1 = dist.evaluate(0, 1).expect("expected to evaluate"); + let x_1 = dist + .evaluate(0, 1, PlatformVersion::latest()) + .expect("expected to evaluate"); assert_eq!(x_1, 1); // This is ln (1/ (1 - 1 + 1)), or basically ln(1) = 1 - let x_2 = dist.evaluate(0, 2).expect("expected to evaluate"); + let x_2 = dist + .evaluate(0, 2, PlatformVersion::latest()) + .expect("expected to evaluate"); assert_eq!(x_2, 0); // This is ln (1/ (1 - 1 + 2)), or basically ln(1/2) = 0 run_test(dist, &steps, 1).await } @@ -2387,12 +2392,24 @@ mod inverted_logarithmic { min_value: None, // min_value: Option, max_value: None, // max_value: Option, }; - let x_1 = dist.evaluate(0, 1).expect("expected to evaluate"); - let x_2 = dist.evaluate(0, 2).expect("expected to evaluate"); - let x_1000 = dist.evaluate(0, 1000).expect("expected to evaluate"); - let x_4000 = dist.evaluate(0, 4000).expect("expected to evaluate"); - let x_5000 = dist.evaluate(0, 5000).expect("expected to evaluate"); - let x_6000 = dist.evaluate(0, 6000).expect("expected to evaluate"); + let x_1 = dist + .evaluate(0, 1, PlatformVersion::latest()) + .expect("expected to evaluate"); + let x_2 = dist + .evaluate(0, 2, PlatformVersion::latest()) + .expect("expected to evaluate"); + let x_1000 = dist + .evaluate(0, 1000, PlatformVersion::latest()) + .expect("expected to evaluate"); + let x_4000 = dist + .evaluate(0, 4000, PlatformVersion::latest()) + .expect("expected to evaluate"); + let x_5000 = dist + .evaluate(0, 5000, PlatformVersion::latest()) + .expect("expected to evaluate"); + let x_6000 = dist + .evaluate(0, 6000, PlatformVersion::latest()) + .expect("expected to evaluate"); assert_eq!(x_1, 85171); assert_eq!(x_2, 78240); assert_eq!(x_1000, 16094); @@ -2470,10 +2487,18 @@ mod inverted_logarithmic { min_value: None, // min_value: Option, max_value: None, // max_value: Option, }; - let x_1 = dist.evaluate(0, 1).expect("expected to evaluate"); - let x_2 = dist.evaluate(0, 2).expect("expected to evaluate"); - let x_1000 = dist.evaluate(0, 1000).expect("expected to evaluate"); - let x_4000 = dist.evaluate(0, 4000).expect("expected to evaluate"); + let x_1 = dist + .evaluate(0, 1, PlatformVersion::latest()) + .expect("expected to evaluate"); + let x_2 = dist + .evaluate(0, 2, PlatformVersion::latest()) + .expect("expected to evaluate"); + let x_1000 = dist + .evaluate(0, 1000, PlatformVersion::latest()) + .expect("expected to evaluate"); + let x_4000 = dist + .evaluate(0, 4000, PlatformVersion::latest()) + .expect("expected to evaluate"); assert_eq!(x_1, 1351); assert_eq!(x_2, 1352); assert_eq!(x_1000, 1984); diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_transition_action/v0/transformer.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_transition_action/v0/transformer.rs index 4cd70d548a2..bbd1899d87f 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_transition_action/v0/transformer.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_transition_action/v0/transformer.rs @@ -446,6 +446,7 @@ impl TokenClaimTransitionActionV0 { start_from_moment_for_distribution, max_cycle_moment, None, + platform_version, )?, ), TokenDistributionRecipient::Identity(identifier) => ( @@ -457,6 +458,7 @@ impl TokenClaimTransitionActionV0 { start_from_moment_for_distribution, max_cycle_moment, None, + platform_version, )?, ), TokenDistributionRecipient::EvonodesByParticipation => { @@ -520,6 +522,7 @@ impl TokenClaimTransitionActionV0 { } } }), + platform_version, )?; ( diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_token_versions/mod.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_token_versions/mod.rs index ede323f1d33..77bc8faccaa 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_token_versions/mod.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_token_versions/mod.rs @@ -1,5 +1,6 @@ pub mod v1; pub mod v2; +pub mod v3; use versioned_feature_core::FeatureVersion; @@ -16,4 +17,8 @@ pub struct DPPTokenVersions { /// v0: uses only minimum_purchase_amount_and_price().1 (vulnerable to schedule swap) /// v1: includes the full serialized TokenPricingSchedule in the hash pub token_set_price_action_id_version: FeatureVersion, + /// Version for distribution function floating-point evaluation. + /// v0: uses std f64 transcendental methods (.powf(), .exp(), .ln()) -- platform-dependent + /// v1: uses libm functions (pow, exp, log) -- cross-platform deterministic + pub distribution_function_evaluate_version: FeatureVersion, } diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_token_versions/v1.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_token_versions/v1.rs index e5114478c72..63b7824cbc1 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_token_versions/v1.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_token_versions/v1.rs @@ -6,4 +6,5 @@ pub const TOKEN_VERSIONS_V1: DPPTokenVersions = DPPTokenVersions { token_contract_info_default_structure_version: 0, token_config_update_action_id_version: 0, token_set_price_action_id_version: 0, + distribution_function_evaluate_version: 0, }; diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_token_versions/v2.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_token_versions/v2.rs index c9f0cc893e0..9109ceefaed 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_token_versions/v2.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_token_versions/v2.rs @@ -6,4 +6,5 @@ pub const TOKEN_VERSIONS_V2: DPPTokenVersions = DPPTokenVersions { token_contract_info_default_structure_version: 0, token_config_update_action_id_version: 1, token_set_price_action_id_version: 1, + distribution_function_evaluate_version: 0, }; diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_token_versions/v3.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_token_versions/v3.rs new file mode 100644 index 00000000000..166a94edda1 --- /dev/null +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_token_versions/v3.rs @@ -0,0 +1,12 @@ +use crate::version::dpp_versions::dpp_token_versions::DPPTokenVersions; + +/// Activates deterministic (libm) distribution function reward math. Wired to +/// `PLATFORM_V14`; `v14.rs` pins that v13 stays on version 0. +pub const TOKEN_VERSIONS_V3: DPPTokenVersions = DPPTokenVersions { + identity_token_info_default_structure_version: 0, + identity_token_status_default_structure_version: 0, + token_contract_info_default_structure_version: 0, + token_config_update_action_id_version: 1, + token_set_price_action_id_version: 1, + distribution_function_evaluate_version: 1, +}; diff --git a/packages/rs-platform-version/src/version/v14.rs b/packages/rs-platform-version/src/version/v14.rs index 7589c485738..a28384b5061 100644 --- a/packages/rs-platform-version/src/version/v14.rs +++ b/packages/rs-platform-version/src/version/v14.rs @@ -10,7 +10,7 @@ use crate::version::dpp_versions::dpp_state_transition_conversion_versions::v2:: use crate::version::dpp_versions::dpp_state_transition_method_versions::v1::STATE_TRANSITION_METHOD_VERSIONS_V1; use crate::version::dpp_versions::dpp_state_transition_serialization_versions::v3::STATE_TRANSITION_SERIALIZATION_VERSIONS_V3; use crate::version::dpp_versions::dpp_state_transition_versions::v3::STATE_TRANSITION_VERSIONS_V3; -use crate::version::dpp_versions::dpp_token_versions::v2::TOKEN_VERSIONS_V2; +use crate::version::dpp_versions::dpp_token_versions::v3::TOKEN_VERSIONS_V3; use crate::version::dpp_versions::dpp_validation_versions::v5::DPP_VALIDATION_VERSIONS_V5; use crate::version::dpp_versions::dpp_voting_versions::v2::VOTING_VERSION_V2; use crate::version::dpp_versions::DPPVersion; @@ -30,7 +30,7 @@ use crate::version::ProtocolVersion; pub const PROTOCOL_VERSION_14: ProtocolVersion = 14; -/// v14 hosts five consensus changes: +/// v14 hosts six consensus changes: /// /// 1. **Contract-level ranked aggregates** (this branch): an index can /// declare that its groups are rankable by an aggregate, so a query like @@ -122,6 +122,16 @@ pub const PROTOCOL_VERSION_14: ProtocolVersion = 14; /// provable over the current or any named window. `unique: true` is /// admitted only for non-overlapping windows (`range == step`) sourced /// from the immutable `$createdAt`. +/// 6. **Deterministic token reward math**: `TOKEN_VERSIONS_V3` bumps +/// `distribution_function_evaluate_version` 0 → 1, so the Polynomial, +/// Exponential, Logarithmic and InvertedLogarithmic perpetual +/// distribution functions evaluate `pow` / `exp` / `ln` through the +/// pinned `libm` crate instead of the host's std `f64` methods, which +/// differ across CPU architectures and libc versions by up to one ulp — +/// enough to flip the truncating integer casts and pay different token +/// amounts on different nodes for the same claim. v13 keeps +/// `TOKEN_VERSIONS_V2` (version 0, std math), so pre-v14 blocks replay +/// byte-for-byte on the nodes that produced them. /// /// The first two are orthogonal by construction: the ranked upgrade decides the /// *property-name* tree type, the demotion decides the *value* tree type @@ -216,7 +226,7 @@ pub const PLATFORM_V14: PlatformVersion = PlatformVersion { document_versions: DOCUMENT_VERSIONS_V4, // changed: document serialization format 3 — the contract version stamp that enables `requiredSince` properties identity_versions: IDENTITY_VERSIONS_V1, voting_versions: VOTING_VERSION_V2, - token_versions: TOKEN_VERSIONS_V2, + token_versions: TOKEN_VERSIONS_V3, // changed: distribution_function_evaluate_version 1 — libm reward math asset_lock_versions: DPP_ASSET_LOCK_VERSIONS_V1, methods: DPP_METHOD_VERSIONS_V3, // changed: daily_withdrawal_limit v2 — a percentage of the total credits a day ago factory_versions: DPP_FACTORY_VERSIONS_V1, @@ -402,4 +412,27 @@ mod tests { 1 ); } + + /// Deterministic (libm) distribution function evaluation activates at v14 + /// and nowhere earlier: the v13 half guards replay of already-committed + /// blocks, the v14 half guards against the activation silently being lost + /// (a `TOKEN_VERSIONS_V*` copy-paste that keeps version 0 would otherwise + /// compile and pass every other test). + #[test] + fn deterministic_distribution_function_evaluation_activates_at_v14() { + assert_eq!( + PLATFORM_V13 + .dpp + .token_versions + .distribution_function_evaluate_version, + 0 + ); + assert_eq!( + PLATFORM_V14 + .dpp + .token_versions + .distribution_function_evaluate_version, + 1 + ); + } }