From 1207e881a96131a21f976d01ab295f45c60e59a9 Mon Sep 17 00:00:00 2001 From: pasta Date: Wed, 8 Apr 2026 12:56:56 -0500 Subject: [PATCH 01/10] fix(dpp)!: version-gate distribution function floating-point evaluation DistributionFunction::evaluate() uses f64 transcendental functions (pow/exp/log) to compute consensus-critical token rewards. The std implementations are platform-dependent, risking consensus divergence between nodes with different architectures or libm versions. Gate these operations behind distribution_function_evaluate_version in DPPTokenVersions. Version 0 preserves the original std behavior (.powf/.exp/.ln) for existing protocol versions. Version 1+ uses deterministic libm functions for cross-platform consistency. Changes: - Add distribution_function_evaluate_version field to DPPTokenVersions - Create TOKEN_VERSIONS_V3 with deterministic evaluation enabled - Add libm 0.2 dependency to rs-dpp - Thread platform_version through evaluate() -> evaluate_interval() -> rewards_in_interval() call chain - Version-gate 4 transcendental call sites: Polynomial (pow), Exponential (exp), Logarithmic (log), InvertedLogarithmic (log) - Add determinism regression tests for all 4 affected variants --- Cargo.lock | 1 + packages/rs-dpp/Cargo.toml | 1 + .../distribution_function/evaluate.rs | 345 +++++++++++++----- .../evaluate_interval.rs | 82 ++++- .../distribution_function/validation.rs | 12 +- .../evaluate_interval.rs | 5 + .../distribution/perpetual/block_based.rs | 24 +- .../v0/transformer.rs | 3 + .../dpp_versions/dpp_token_versions/mod.rs | 5 + .../dpp_versions/dpp_token_versions/v1.rs | 1 + .../dpp_versions/dpp_token_versions/v2.rs | 1 + .../dpp_versions/dpp_token_versions/v3.rs | 10 + 12 files changed, 376 insertions(+), 114 deletions(-) create mode 100644 packages/rs-platform-version/src/version/dpp_versions/dpp_token_versions/v3.rs 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..d8bb2ce7627 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" 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..bca5524ff62 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 @@ -4,6 +4,8 @@ use crate::data_contract::associated_token::token_perpetual_distribution::distri MAX_DISTRIBUTION_PARAM, }; use crate::ProtocolError; +use libm::{exp, log, pow}; +use platform_version::version::PlatformVersion; impl DistributionFunction { /// Evaluates the distribution function at the given period `x`. @@ -19,6 +21,7 @@ impl DistributionFunction { &self, contract_registration_step: u64, x: u64, + platform_version: &PlatformVersion, ) -> Result { match self { DistributionFunction::FixedAmount { amount: n } => { @@ -222,7 +225,14 @@ impl DistributionFunction { )); } - let diff_exp = (diff as f64).powf(exponent); + let diff_exp = match platform_version + .dpp + .token_versions + .distribution_function_evaluate_version + { + 0 => (diff as f64).powf(exponent), + _ => pow(diff as f64, exponent), + }; if !diff_exp.is_finite() { return if diff_exp.is_sign_positive() { @@ -326,7 +336,15 @@ 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 = match platform_version + .dpp + .token_versions + .distribution_function_evaluate_version + { + 0 => exponent.exp(), + _ => 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,7 +418,14 @@ impl DistributionFunction { (*m as f64) * (diff as f64) / (*n as f64) }; - let log_val = argument.ln(); + let log_val = match platform_version + .dpp + .token_versions + .distribution_function_evaluate_version + { + 0 => argument.ln(), + _ => log(argument), + }; // Ensure the computed value is finite and within the u64 range. if !log_val.is_finite() || log_val > (u64::MAX as f64) { @@ -538,7 +563,14 @@ impl DistributionFunction { )); } - let log_val = argument.ln(); + let log_val = match platform_version + .dpp + .token_versions + .distribution_function_evaluate_version + { + 0 => argument.ln(), + _ => log(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 +638,15 @@ impl DistributionFunction { #[cfg(test)] mod tests { use super::*; + use platform_version::version::PlatformVersion; use std::collections::BTreeMap; #[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, PlatformVersion::latest()).unwrap(), 100); + assert_eq!(distribution.evaluate(0, 50, PlatformVersion::latest()).unwrap(), 100); + assert_eq!(distribution.evaluate(0, 1000, PlatformVersion::latest()).unwrap(), 100); } #[test] @@ -624,12 +657,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, PlatformVersion::latest()).unwrap(), 100); + assert_eq!(distribution.evaluate(0, 5, PlatformVersion::latest()).unwrap(), 100); + assert_eq!(distribution.evaluate(0, 10, PlatformVersion::latest()).unwrap(), 50); + assert_eq!(distribution.evaluate(0, 15, PlatformVersion::latest()).unwrap(), 50); + assert_eq!(distribution.evaluate(0, 20, PlatformVersion::latest()).unwrap(), 25); + assert_eq!(distribution.evaluate(0, 30, PlatformVersion::latest()).unwrap(), 25); } #[test] @@ -645,12 +678,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, PlatformVersion::latest()).unwrap(), 100); + assert_eq!(distribution.evaluate(0, 9, PlatformVersion::latest()).unwrap(), 100); + assert_eq!(distribution.evaluate(0, 10, PlatformVersion::latest()).unwrap(), 50); + assert_eq!(distribution.evaluate(0, 20, PlatformVersion::latest()).unwrap(), 25); + assert_eq!(distribution.evaluate(0, 30, PlatformVersion::latest()).unwrap(), 12); + assert_eq!(distribution.evaluate(0, 40, PlatformVersion::latest()).unwrap(), 10); // Should not go below min_value } #[test] @@ -667,7 +700,7 @@ mod tests { }; assert!(matches!( - distribution.evaluate(0, 10), + distribution.evaluate(0, 10, PlatformVersion::latest()), Err(ProtocolError::DivideByZero(_)) )); } @@ -679,7 +712,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, PlatformVersion::latest()).unwrap(); assert!( (10..=100).contains(&result), "Random value {} is out of range for x = {}", @@ -694,7 +727,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, PlatformVersion::latest()).unwrap(); assert_eq!( result, 42, "Expected fixed output 42, got {} for x = {}", @@ -707,7 +740,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, PlatformVersion::latest()); assert!( matches!(result, Err(ProtocolError::Overflow(_))), "Expected ProtocolError::Overflow but got {:?}", @@ -719,8 +752,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, PlatformVersion::latest()).unwrap(); + let value2 = distribution.evaluate(0, 42, PlatformVersion::latest()).unwrap(); assert_eq!( value1, value2, @@ -732,8 +765,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, PlatformVersion::latest()).unwrap(); + let value2 = distribution.evaluate(0, 2, PlatformVersion::latest()).unwrap(); assert_ne!( value1, value2, @@ -754,10 +787,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, PlatformVersion::latest()).unwrap(), 50); + assert_eq!(distribution.evaluate(0, 2, PlatformVersion::latest()).unwrap(), 60); + assert_eq!(distribution.evaluate(0, 4, PlatformVersion::latest()).unwrap(), 70); + assert_eq!(distribution.evaluate(0, 6, PlatformVersion::latest()).unwrap(), 80); } #[test] @@ -771,9 +804,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, PlatformVersion::latest()).unwrap(), 100); + assert_eq!(distribution.evaluate(0, 10, PlatformVersion::latest()).unwrap(), 50); + assert_eq!(distribution.evaluate(0, 20, PlatformVersion::latest()).unwrap(), 10); // Should not go below min_value } #[test] @@ -788,7 +821,7 @@ mod tests { }; assert!(matches!( - distribution.evaluate(0, 10), + distribution.evaluate(0, 10, PlatformVersion::latest()), Err(ProtocolError::DivideByZero(_)) )); } @@ -810,10 +843,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, PlatformVersion::latest()).unwrap(), 0); + assert_eq!(distribution.evaluate(0, 2, PlatformVersion::latest()).unwrap(), 18); + assert_eq!(distribution.evaluate(0, 3, PlatformVersion::latest()).unwrap(), 28); + assert_eq!(distribution.evaluate(0, 4, PlatformVersion::latest()).unwrap(), 42); } #[test] @@ -830,7 +863,7 @@ mod tests { max_value: None, }; - let result = distribution.evaluate(0, 100000).expect("expected value"); + let result = distribution.evaluate(0, 100000, PlatformVersion::latest()).expect("expected value"); assert_eq!(result, MAX_DISTRIBUTION_PARAM); } @@ -849,7 +882,35 @@ 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, PlatformVersion::latest()).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, + }; + + // cbrt(125) is exactly 5. The std f64 powf() on some platforms rounds + // the intermediate result below 5.0 and truncates to 4 when cast to u64. + // The deterministic libm path (version >= 1) must always return 5. + let mut deterministic_version = PlatformVersion::latest().clone(); + deterministic_version + .dpp + .token_versions + .distribution_function_evaluate_version = 1; + assert_eq!( + distribution.evaluate(0, 125, &deterministic_version).unwrap(), + 5 + ); } // Test: Negative coefficient a (should flip the sign) @@ -867,7 +928,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, PlatformVersion::latest()).unwrap(), 0); } // Test: Non-zero shift parameter s (shifting the x coordinate) @@ -885,9 +946,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, PlatformVersion::latest()).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, PlatformVersion::latest()).unwrap(), 12); } // Test: Non-zero offset o (shifting the base of the power) @@ -906,7 +967,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, PlatformVersion::latest()).unwrap(), 42); } // Test: Linear function when exponent is 1 (m = 1, n = 1) @@ -924,7 +985,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, PlatformVersion::latest()).unwrap(), 35); } // Test: Cubic function (m = 3, n = 1) @@ -942,7 +1003,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, PlatformVersion::latest()).unwrap(), 64); } // Test: Combination of non-zero offset and shift @@ -961,7 +1022,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, PlatformVersion::latest()).unwrap(), 16); } } mod exp { @@ -980,8 +1041,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, PlatformVersion::latest()).unwrap(), 11); + assert!(distribution.evaluate(0, 10, PlatformVersion::latest()).unwrap() > 20); } #[test] @@ -999,7 +1060,7 @@ mod tests { }; assert!(matches!( - distribution.evaluate(0, 10), + distribution.evaluate(0, 10, PlatformVersion::latest()), Err(ProtocolError::DivideByZero(_)) )); } @@ -1018,9 +1079,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, PlatformVersion::latest()).unwrap(), 7); + assert_eq!(distribution.evaluate(0, 5, PlatformVersion::latest()).unwrap(), 301); + assert_eq!(distribution.evaluate(0, 10, PlatformVersion::latest()).unwrap(), 44057); } #[test] @@ -1037,9 +1098,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, PlatformVersion::latest()).unwrap(), 0); + assert_eq!(distribution.evaluate(0, 50, PlatformVersion::latest()).unwrap(), 14); + assert_eq!(distribution.evaluate(0, 100, PlatformVersion::latest()).unwrap(), 2202); } #[test] @@ -1056,11 +1117,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, PlatformVersion::latest()).unwrap(), 1); + assert_eq!(distribution.evaluate(0, 2, PlatformVersion::latest()).unwrap(), 2980); + assert_eq!(distribution.evaluate(0, 4, PlatformVersion::latest()).unwrap(), 8886110); + assert_eq!(distribution.evaluate(0, 10, PlatformVersion::latest()).unwrap(), 100000000); + assert_eq!(distribution.evaluate(0, 100000, PlatformVersion::latest()).unwrap(), 100000000); } #[test] @@ -1077,9 +1138,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, PlatformVersion::latest()).unwrap(), 12); // f(0) = (2 * e^(-1 * (0 - 0 + 0) / 1)) / 1 + 10 + assert_eq!(distribution.evaluate(0, 5, PlatformVersion::latest()).unwrap(), 10); + assert_eq!(distribution.evaluate(0, 10000, PlatformVersion::latest()).unwrap(), 10); } #[test] @@ -1096,9 +1157,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, PlatformVersion::latest()).unwrap(), 12); // f(0) = (2 * e^(-1 * (0 - 0 + 0) / 1)) / 1 + 10 + assert_eq!(distribution.evaluate(0, 5, PlatformVersion::latest()).unwrap(), 11); + assert_eq!(distribution.evaluate(0, 100, PlatformVersion::latest()).unwrap(), 11); } #[test] @@ -1116,12 +1177,12 @@ mod tests { }; assert_eq!( - distribution.evaluate(0, 0).unwrap(), + distribution.evaluate(0, 0, PlatformVersion::latest()).unwrap(), 11, "Function should start at the max value" ); assert_eq!( - distribution.evaluate(0, 5).unwrap(), + distribution.evaluate(0, 5, PlatformVersion::latest()).unwrap(), 11, "Function should be clamped at max value" ); @@ -1141,13 +1202,57 @@ mod tests { max_value: None, }; - let result = distribution.evaluate(0, 100000); + let result = distribution.evaluate(0, 100000, PlatformVersion::latest()); 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 mut deterministic_version = PlatformVersion::latest().clone(); + deterministic_version + .dpp + .token_versions + .distribution_function_evaluate_version = 1; + let v1_result = distribution + .evaluate(0, 2, &deterministic_version) + .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, &deterministic_version) + .unwrap(); + assert_eq!(v1_result2, 7); + } } mod log { use super::*; @@ -1165,8 +1270,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, PlatformVersion::latest()).unwrap(), 5); + assert!(distribution.evaluate(0, 10, PlatformVersion::latest()).unwrap() > 5); } #[test] @@ -1183,8 +1288,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, PlatformVersion::latest()).unwrap(), 7); // Clamped to min_value + assert!(distribution.evaluate(0, 10, PlatformVersion::latest()).unwrap() <= 20); // Should not exceed max_value } #[test] @@ -1202,7 +1307,7 @@ mod tests { }; assert!(matches!( - distribution.evaluate(0, 1), + distribution.evaluate(0, 1, PlatformVersion::latest()), Err(ProtocolError::Overflow(_)) )); } @@ -1221,7 +1326,7 @@ mod tests { max_value: None, }; - let result = distribution.evaluate(0, 100); + let result = distribution.evaluate(0, 100, PlatformVersion::latest()); assert!(result.is_ok()); assert!(result.unwrap() > 10); // Function should increase over time } @@ -1241,7 +1346,7 @@ mod tests { }; assert!(matches!( - distribution.evaluate(0, 10), + distribution.evaluate(0, 10, PlatformVersion::latest()), Err(ProtocolError::DivideByZero(_)) )); } @@ -1261,10 +1366,37 @@ mod tests { }; assert!(matches!( - distribution.evaluate(0, 10), + distribution.evaluate(0, 10, PlatformVersion::latest()), 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 mut deterministic_version = PlatformVersion::latest().clone(); + deterministic_version + .dpp + .token_versions + .distribution_function_evaluate_version = 1; + let v1_result = distribution + .evaluate(0, 100, &deterministic_version) + .unwrap(); + assert_eq!(v1_result, 46); + } } mod inverted_log { use super::*; @@ -1282,8 +1414,8 @@ 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, PlatformVersion::latest()).unwrap() > distribution.evaluate(0, 5, PlatformVersion::latest()).unwrap()); + assert!(distribution.evaluate(0, 5, PlatformVersion::latest()).unwrap() > distribution.evaluate(0, 10, PlatformVersion::latest()).unwrap()); } #[test] @@ -1301,9 +1433,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, PlatformVersion::latest()).unwrap(); + let val2000 = distribution.evaluate(0, 2000, PlatformVersion::latest()).unwrap(); + let val3000 = distribution.evaluate(0, 3000, PlatformVersion::latest()).unwrap(); assert!(val1000 < val2000, "Function should be increasing"); assert!(val2000 < val3000, "Function should be increasing"); @@ -1323,7 +1455,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, PlatformVersion::latest()).unwrap(), 0); // Should be clamped to 0 } #[test] @@ -1340,7 +1472,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, PlatformVersion::latest()).unwrap(), 7); // Should be clamped to min_value } #[test] @@ -1358,7 +1490,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, PlatformVersion::latest()).unwrap(), 20); // Should be clamped to max_value } #[test] @@ -1376,7 +1508,7 @@ mod tests { }; assert!(matches!( - distribution.evaluate(0, 1), + distribution.evaluate(0, 1, PlatformVersion::latest()), Err(ProtocolError::Overflow(_)) )); } @@ -1396,7 +1528,7 @@ mod tests { }; assert!(matches!( - distribution.evaluate(0, 10), + distribution.evaluate(0, 10, PlatformVersion::latest()), Err(ProtocolError::DivideByZero(_)) )); } @@ -1416,7 +1548,7 @@ mod tests { }; assert!(matches!( - distribution.evaluate(0, 10), + distribution.evaluate(0, 10, PlatformVersion::latest()), Err(ProtocolError::DivideByZero(_)) )); } @@ -1436,12 +1568,12 @@ mod tests { }; assert_eq!( - distribution.evaluate(0, 0).unwrap(), + distribution.evaluate(0, 0, PlatformVersion::latest()).unwrap(), 1, "Function should start at the max value" ); assert_eq!( - distribution.evaluate(0, 200).unwrap(), + distribution.evaluate(0, 200, PlatformVersion::latest()).unwrap(), 10, "Function should remain clamped at max value" ); @@ -1462,10 +1594,37 @@ mod tests { }; assert_eq!( - distribution.evaluate(0, 1000).unwrap(), + distribution.evaluate(0, 1000, PlatformVersion::latest()).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=1 (with o=1, so 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 mut deterministic_version = PlatformVersion::latest().clone(); + deterministic_version + .dpp + .token_versions + .distribution_function_evaluate_version = 1; + let v1_result = distribution + .evaluate(0, 0, &deterministic_version) + .unwrap(); + assert_eq!(v1_result, 51); + } } } 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..29942129607 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,5 +1,4 @@ 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; @@ -1571,6 +1570,7 @@ impl DistributionFunction { interval_end_included: RewardDistributionMoment, step: RewardDistributionMoment, get_epoch_reward_ratio: Option, + platform_version: &PlatformVersion, ) -> Result where F: Fn(RangeInclusive) -> Option, @@ -1650,7 +1650,7 @@ impl DistributionFunction { while current_point <= last_step { let base_amount = - self.evaluate(distribution_start_step.to_u64(), current_point.to_u64())?; + self.evaluate(distribution_start_step.to_u64(), current_point.to_u64(), platform_version)?; let amount = if let ( RewardDistributionMoment::EpochBasedMoment(epoch_index), @@ -1715,6 +1715,7 @@ impl DistributionFunction { step: RewardDistributionMoment, get_epoch_reward_ratio: Option, is_first_claim: bool, + platform_version: &PlatformVersion, ) -> Result where F: Fn(RangeInclusive) -> Option, @@ -1839,7 +1840,7 @@ impl DistributionFunction { while current_point <= last_step { let base_amount = - self.evaluate(distribution_start_step.to_u64(), current_point.to_u64())?; + self.evaluate(distribution_start_step.to_u64(), current_point.to_u64(), platform_version)?; let (amount, reward_ratio) = if let ( RewardDistributionMoment::EpochBasedMoment(epoch_index), @@ -1915,6 +1916,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -1946,6 +1948,7 @@ mod tests { step, None::) -> Option>, false, + PlatformVersion::latest(), ) .unwrap(); @@ -1976,6 +1979,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -2007,6 +2011,7 @@ mod tests { step, None::) -> Option>, false, + PlatformVersion::latest(), ) .unwrap(); @@ -2048,6 +2053,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -2086,6 +2092,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -2124,6 +2131,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -2165,6 +2173,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -2206,6 +2215,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -2247,6 +2257,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -2288,6 +2299,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -2318,6 +2330,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -2359,6 +2372,7 @@ mod tests { step, Some(get_ratio), true, + PlatformVersion::latest(), ) .unwrap(); @@ -2383,6 +2397,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -2415,6 +2430,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -2451,6 +2467,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -2476,6 +2493,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -2504,6 +2522,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -2542,6 +2561,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -2575,6 +2595,7 @@ mod tests { step, None::) -> Option>, false, + PlatformVersion::latest(), ) .unwrap(); @@ -2621,6 +2642,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -2651,6 +2673,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -2693,6 +2716,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -2718,6 +2742,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -2743,6 +2768,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -2772,6 +2798,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -2813,6 +2840,7 @@ mod tests { step, None::) -> Option>, false, + PlatformVersion::latest(), ) .unwrap(); @@ -2855,6 +2883,7 @@ mod tests { step, None::) -> Option>, false, + PlatformVersion::latest(), ) .unwrap(); @@ -2885,6 +2914,7 @@ mod tests { step, None::) -> Option>, false, + PlatformVersion::latest(), ) .unwrap(); @@ -2911,6 +2941,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -2942,6 +2973,7 @@ mod tests { step, None::) -> Option>, false, + PlatformVersion::latest(), ) .unwrap(); @@ -2980,6 +3012,7 @@ mod tests { step, None::) -> Option>, false, + PlatformVersion::latest(), ) .unwrap(); @@ -3018,6 +3051,7 @@ mod tests { step, None::) -> Option>, false, + PlatformVersion::latest(), ) .unwrap(); @@ -3059,6 +3093,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -3100,6 +3135,7 @@ mod tests { step, None::) -> Option>, false, + PlatformVersion::latest(), ) .unwrap(); @@ -3141,6 +3177,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -3182,6 +3219,7 @@ mod tests { step, None::) -> Option>, false, + PlatformVersion::latest(), ) .unwrap(); @@ -3223,6 +3261,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -3264,6 +3303,7 @@ mod tests { step, None::) -> Option>, false, + PlatformVersion::latest(), ) .unwrap(); @@ -3294,6 +3334,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -3334,6 +3375,7 @@ mod tests { step, Some(get_ratio), true, + PlatformVersion::latest(), ) .unwrap(); @@ -3358,6 +3400,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -3393,6 +3436,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -3431,6 +3475,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -3464,6 +3509,7 @@ mod tests { step, None::) -> Option>, false, + PlatformVersion::latest(), ) .unwrap(); @@ -3494,6 +3540,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -3535,6 +3582,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -3575,6 +3623,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -3607,6 +3656,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -3638,6 +3688,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -3671,6 +3722,7 @@ mod tests { step, None::) -> Option>, false, + PlatformVersion::latest(), ) .unwrap(); @@ -3714,6 +3766,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -3757,6 +3810,7 @@ mod tests { step, None::) -> Option>, false, + PlatformVersion::latest(), ) .unwrap(); @@ -3797,6 +3851,7 @@ mod tests { step, None::) -> Option>, false, + PlatformVersion::latest(), ) .unwrap(); @@ -3837,6 +3892,7 @@ mod tests { step, None::) -> Option>, false, + PlatformVersion::latest(), ) .unwrap(); @@ -3880,6 +3936,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -3928,6 +3985,7 @@ mod tests { step, None::) -> Option>, false, + PlatformVersion::latest(), ) .unwrap(); @@ -3976,6 +4034,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -4024,6 +4083,7 @@ mod tests { step, None::) -> Option>, false, + PlatformVersion::latest(), ) .unwrap(); @@ -4072,6 +4132,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -4120,6 +4181,7 @@ mod tests { step, None::) -> Option>, false, + PlatformVersion::latest(), ) .unwrap(); @@ -4168,6 +4230,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -4216,6 +4279,7 @@ mod tests { step, None::) -> Option>, false, + PlatformVersion::latest(), ) .unwrap(); @@ -4253,6 +4317,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -4290,6 +4355,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -4315,6 +4381,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -4340,6 +4407,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -4379,6 +4447,7 @@ mod tests { step, Some(get_ratio), true, + PlatformVersion::latest(), ) .unwrap(); @@ -4403,6 +4472,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -4435,6 +4505,7 @@ mod tests { step, None::) -> Option>, true, + PlatformVersion::latest(), ) .unwrap(); @@ -4477,6 +4548,7 @@ mod tests { step, Some(get_ratio), false, + PlatformVersion::latest(), ) .unwrap(); @@ -4520,6 +4592,7 @@ mod tests { step, Some(get_ratio), true, // first claim + PlatformVersion::latest(), ) .unwrap(); @@ -4563,6 +4636,7 @@ mod tests { step, Some(get_ratio), false, + PlatformVersion::latest(), ) .unwrap(); @@ -4620,6 +4694,7 @@ mod tests { step, Some(get_ratio), false, + PlatformVersion::latest(), ) .unwrap(); @@ -4678,6 +4753,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..3e2883cda76 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 @@ -2353,9 +2353,9 @@ 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 +2387,12 @@ 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 +2470,10 @@ 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..b669be8c9cc --- /dev/null +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_token_versions/v3.rs @@ -0,0 +1,10 @@ +use crate::version::dpp_versions::dpp_token_versions::DPPTokenVersions; + +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, +}; From a887d1e88eea1cf595f1b22bd29ce45133003ac8 Mon Sep 17 00:00:00 2001 From: pasta Date: Mon, 1 Jun 2026 16:18:53 -0500 Subject: [PATCH 02/10] fix(dpp): fail closed on unknown distribution_function_evaluate_version Replace wildcard libm dispatch with explicit version arms (0/1) ending in UnknownVersionMismatch across all four evaluate.rs call sites. Fix a misleading determinism-test comment and document that TOKEN_VERSIONS_V3 has no PlatformVersion consumer yet. --- .../distribution_function/evaluate.rs | 39 ++++++++++++++++--- .../dpp_versions/dpp_token_versions/v3.rs | 3 ++ 2 files changed, 37 insertions(+), 5 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 bca5524ff62..34baf738954 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 @@ -231,7 +231,14 @@ impl DistributionFunction { .distribution_function_evaluate_version { 0 => (diff as f64).powf(exponent), - _ => pow(diff as f64, exponent), + 1 => pow(diff as f64, exponent), + version => { + return Err(ProtocolError::UnknownVersionMismatch { + method: "DistributionFunction::evaluate (Polynomial)".to_string(), + known_versions: vec![0, 1], + received: version, + }) + } }; if !diff_exp.is_finite() { @@ -342,7 +349,14 @@ impl DistributionFunction { .distribution_function_evaluate_version { 0 => exponent.exp(), - _ => exp(exponent), + 1 => exp(exponent), + version => { + return Err(ProtocolError::UnknownVersionMismatch { + method: "DistributionFunction::evaluate (Exponential)".to_string(), + known_versions: vec![0, 1], + received: version, + }) + } }; let value = ((*a as f64) * exp_val / (*d as f64)) + (*b as f64); if let Some(max_value) = max_value { @@ -424,7 +438,14 @@ impl DistributionFunction { .distribution_function_evaluate_version { 0 => argument.ln(), - _ => log(argument), + 1 => log(argument), + version => { + return Err(ProtocolError::UnknownVersionMismatch { + method: "DistributionFunction::evaluate (Logarithmic)".to_string(), + known_versions: vec![0, 1], + received: version, + }) + } }; // Ensure the computed value is finite and within the u64 range. @@ -569,7 +590,15 @@ impl DistributionFunction { .distribution_function_evaluate_version { 0 => argument.ln(), - _ => log(argument), + 1 => log(argument), + version => { + return Err(ProtocolError::UnknownVersionMismatch { + method: "DistributionFunction::evaluate (InvertedLogarithmic)" + .to_string(), + known_versions: vec![0, 1], + received: version, + }) + } }; // Ensure the computed value is finite and within the u64 range. @@ -1603,7 +1632,7 @@ mod tests { #[test] fn test_inverted_logarithmic_deterministic_libm_path() { // f(x) = 10 * ln(100 / (1 * x)) / 1 + 5 - // At x=1 (with o=1, so arg = 100/1 = 100): ln(100) ≈ 4.605, * 10 = 46.05 + 5 = 51 + // 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, 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 index b669be8c9cc..374f595f765 100644 --- 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 @@ -1,5 +1,8 @@ use crate::version::dpp_versions::dpp_token_versions::DPPTokenVersions; +/// NOTE: Not yet wired to any `PlatformVersion::PLATFORM_V*`. This constant sets +/// `distribution_function_evaluate_version: 1` (deterministic libm reward math), but +/// activation is deferred to a follow-up `PLATFORM_V13` PR. Until then it has no consumer. pub const TOKEN_VERSIONS_V3: DPPTokenVersions = DPPTokenVersions { identity_token_info_default_structure_version: 0, identity_token_status_default_structure_version: 0, From 7a1433b73ac07d66942b6b17fde9a6db32f8ff83 Mon Sep 17 00:00:00 2001 From: pasta Date: Mon, 7 Sep 2026 22:58:45 -0500 Subject: [PATCH 03/10] style(dpp): rustfmt the distribution function evaluation call sites The added platform_version argument pushed 44 hunks in evaluate.rs, evaluate_interval.rs and the drive-abci block_based tests past 100 columns, which failed CI at the fmt gate before any Rust test ran. Also allow clippy::too_many_arguments on evaluate_interval_with_explanation (now 8 args) so the -D warnings clippy step passes, and import PlatformVersion in the drive-abci inverted_logarithmic test module, which the rebase onto v4.2-dev left without it. Co-Authored-By: Claude Fable 5.1 --- .../distribution_function/evaluate.rs | 510 +++++++++++++++--- .../evaluate_interval.rs | 15 +- .../distribution/perpetual/block_based.rs | 49 +- 3 files changed, 474 insertions(+), 100 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 34baf738954..021926b434e 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 @@ -673,9 +673,24 @@ mod tests { #[test] fn test_fixed_amount() { let distribution = DistributionFunction::FixedAmount { amount: 100 }; - assert_eq!(distribution.evaluate(0, 0, PlatformVersion::latest()).unwrap(), 100); - assert_eq!(distribution.evaluate(0, 50, PlatformVersion::latest()).unwrap(), 100); - assert_eq!(distribution.evaluate(0, 1000, PlatformVersion::latest()).unwrap(), 100); + assert_eq!( + distribution + .evaluate(0, 0, PlatformVersion::latest()) + .unwrap(), + 100 + ); + assert_eq!( + distribution + .evaluate(0, 50, PlatformVersion::latest()) + .unwrap(), + 100 + ); + assert_eq!( + distribution + .evaluate(0, 1000, PlatformVersion::latest()) + .unwrap(), + 100 + ); } #[test] @@ -686,12 +701,42 @@ mod tests { steps.insert(20, 25); let distribution = DistributionFunction::Stepwise(steps); - assert_eq!(distribution.evaluate(0, 0, PlatformVersion::latest()).unwrap(), 100); - assert_eq!(distribution.evaluate(0, 5, PlatformVersion::latest()).unwrap(), 100); - assert_eq!(distribution.evaluate(0, 10, PlatformVersion::latest()).unwrap(), 50); - assert_eq!(distribution.evaluate(0, 15, PlatformVersion::latest()).unwrap(), 50); - assert_eq!(distribution.evaluate(0, 20, PlatformVersion::latest()).unwrap(), 25); - assert_eq!(distribution.evaluate(0, 30, PlatformVersion::latest()).unwrap(), 25); + assert_eq!( + distribution + .evaluate(0, 0, PlatformVersion::latest()) + .unwrap(), + 100 + ); + assert_eq!( + distribution + .evaluate(0, 5, PlatformVersion::latest()) + .unwrap(), + 100 + ); + assert_eq!( + distribution + .evaluate(0, 10, PlatformVersion::latest()) + .unwrap(), + 50 + ); + assert_eq!( + distribution + .evaluate(0, 15, PlatformVersion::latest()) + .unwrap(), + 50 + ); + assert_eq!( + distribution + .evaluate(0, 20, PlatformVersion::latest()) + .unwrap(), + 25 + ); + assert_eq!( + distribution + .evaluate(0, 30, PlatformVersion::latest()) + .unwrap(), + 25 + ); } #[test] @@ -707,12 +752,42 @@ mod tests { min_value: Some(10), }; - assert_eq!(distribution.evaluate(0, 0, PlatformVersion::latest()).unwrap(), 100); - assert_eq!(distribution.evaluate(0, 9, PlatformVersion::latest()).unwrap(), 100); - assert_eq!(distribution.evaluate(0, 10, PlatformVersion::latest()).unwrap(), 50); - assert_eq!(distribution.evaluate(0, 20, PlatformVersion::latest()).unwrap(), 25); - assert_eq!(distribution.evaluate(0, 30, PlatformVersion::latest()).unwrap(), 12); - assert_eq!(distribution.evaluate(0, 40, PlatformVersion::latest()).unwrap(), 10); // Should not go below min_value + assert_eq!( + distribution + .evaluate(0, 0, PlatformVersion::latest()) + .unwrap(), + 100 + ); + assert_eq!( + distribution + .evaluate(0, 9, PlatformVersion::latest()) + .unwrap(), + 100 + ); + assert_eq!( + distribution + .evaluate(0, 10, PlatformVersion::latest()) + .unwrap(), + 50 + ); + assert_eq!( + distribution + .evaluate(0, 20, PlatformVersion::latest()) + .unwrap(), + 25 + ); + assert_eq!( + distribution + .evaluate(0, 30, PlatformVersion::latest()) + .unwrap(), + 12 + ); + assert_eq!( + distribution + .evaluate(0, 40, PlatformVersion::latest()) + .unwrap(), + 10 + ); // Should not go below min_value } #[test] @@ -741,7 +816,9 @@ mod tests { let distribution = DistributionFunction::Random { min: 10, max: 100 }; for x in 0..100 { - let result = distribution.evaluate(0, x, PlatformVersion::latest()).unwrap(); + let result = distribution + .evaluate(0, x, PlatformVersion::latest()) + .unwrap(); assert!( (10..=100).contains(&result), "Random value {} is out of range for x = {}", @@ -756,7 +833,9 @@ mod tests { let distribution = DistributionFunction::Random { min: 42, max: 42 }; for x in 0..10 { - let result = distribution.evaluate(0, x, PlatformVersion::latest()).unwrap(); + let result = distribution + .evaluate(0, x, PlatformVersion::latest()) + .unwrap(); assert_eq!( result, 42, "Expected fixed output 42, got {} for x = {}", @@ -781,8 +860,12 @@ mod tests { fn test_random_distribution_deterministic_for_same_x() { let distribution = DistributionFunction::Random { min: 10, max: 100 }; - let value1 = distribution.evaluate(0, 42, PlatformVersion::latest()).unwrap(); - let value2 = distribution.evaluate(0, 42, PlatformVersion::latest()).unwrap(); + let value1 = distribution + .evaluate(0, 42, PlatformVersion::latest()) + .unwrap(); + let value2 = distribution + .evaluate(0, 42, PlatformVersion::latest()) + .unwrap(); assert_eq!( value1, value2, @@ -794,8 +877,12 @@ mod tests { fn test_random_distribution_varies_for_different_x() { let distribution = DistributionFunction::Random { min: 10, max: 100 }; - let value1 = distribution.evaluate(0, 1, PlatformVersion::latest()).unwrap(); - let value2 = distribution.evaluate(0, 2, PlatformVersion::latest()).unwrap(); + let value1 = distribution + .evaluate(0, 1, PlatformVersion::latest()) + .unwrap(); + let value2 = distribution + .evaluate(0, 2, PlatformVersion::latest()) + .unwrap(); assert_ne!( value1, value2, @@ -816,10 +903,30 @@ mod tests { max_value: None, }; - assert_eq!(distribution.evaluate(0, 0, PlatformVersion::latest()).unwrap(), 50); - assert_eq!(distribution.evaluate(0, 2, PlatformVersion::latest()).unwrap(), 60); - assert_eq!(distribution.evaluate(0, 4, PlatformVersion::latest()).unwrap(), 70); - assert_eq!(distribution.evaluate(0, 6, PlatformVersion::latest()).unwrap(), 80); + assert_eq!( + distribution + .evaluate(0, 0, PlatformVersion::latest()) + .unwrap(), + 50 + ); + assert_eq!( + distribution + .evaluate(0, 2, PlatformVersion::latest()) + .unwrap(), + 60 + ); + assert_eq!( + distribution + .evaluate(0, 4, PlatformVersion::latest()) + .unwrap(), + 70 + ); + assert_eq!( + distribution + .evaluate(0, 6, PlatformVersion::latest()) + .unwrap(), + 80 + ); } #[test] @@ -833,9 +940,24 @@ mod tests { max_value: None, }; - assert_eq!(distribution.evaluate(0, 0, PlatformVersion::latest()).unwrap(), 100); - assert_eq!(distribution.evaluate(0, 10, PlatformVersion::latest()).unwrap(), 50); - assert_eq!(distribution.evaluate(0, 20, PlatformVersion::latest()).unwrap(), 10); // Should not go below min_value + assert_eq!( + distribution + .evaluate(0, 0, PlatformVersion::latest()) + .unwrap(), + 100 + ); + assert_eq!( + distribution + .evaluate(0, 10, PlatformVersion::latest()) + .unwrap(), + 50 + ); + assert_eq!( + distribution + .evaluate(0, 20, PlatformVersion::latest()) + .unwrap(), + 10 + ); // Should not go below min_value } #[test] @@ -872,10 +994,30 @@ mod tests { max_value: None, }; - assert_eq!(distribution.evaluate(0, 0, PlatformVersion::latest()).unwrap(), 0); - assert_eq!(distribution.evaluate(0, 2, PlatformVersion::latest()).unwrap(), 18); - assert_eq!(distribution.evaluate(0, 3, PlatformVersion::latest()).unwrap(), 28); - assert_eq!(distribution.evaluate(0, 4, PlatformVersion::latest()).unwrap(), 42); + assert_eq!( + distribution + .evaluate(0, 0, PlatformVersion::latest()) + .unwrap(), + 0 + ); + assert_eq!( + distribution + .evaluate(0, 2, PlatformVersion::latest()) + .unwrap(), + 18 + ); + assert_eq!( + distribution + .evaluate(0, 3, PlatformVersion::latest()) + .unwrap(), + 28 + ); + assert_eq!( + distribution + .evaluate(0, 4, PlatformVersion::latest()) + .unwrap(), + 42 + ); } #[test] @@ -892,7 +1034,9 @@ mod tests { max_value: None, }; - let result = distribution.evaluate(0, 100000, PlatformVersion::latest()).expect("expected value"); + let result = distribution + .evaluate(0, 100000, PlatformVersion::latest()) + .expect("expected value"); assert_eq!(result, MAX_DISTRIBUTION_PARAM); } @@ -911,7 +1055,12 @@ 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, PlatformVersion::latest()).unwrap(), 8); + assert_eq!( + distribution + .evaluate(0, 4, PlatformVersion::latest()) + .unwrap(), + 8 + ); } #[test] @@ -937,7 +1086,9 @@ mod tests { .token_versions .distribution_function_evaluate_version = 1; assert_eq!( - distribution.evaluate(0, 125, &deterministic_version).unwrap(), + distribution + .evaluate(0, 125, &deterministic_version) + .unwrap(), 5 ); } @@ -957,7 +1108,12 @@ mod tests { max_value: None, }; // f(x) = -1 * (x^2). For x = 3: -1 * (3^2) = -9. - assert_eq!(distribution.evaluate(0, 3, PlatformVersion::latest()).unwrap(), 0); + assert_eq!( + distribution + .evaluate(0, 3, PlatformVersion::latest()) + .unwrap(), + 0 + ); } // Test: Non-zero shift parameter s (shifting the x coordinate) @@ -975,9 +1131,19 @@ 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, PlatformVersion::latest()).unwrap(), 0); + assert_eq!( + distribution + .evaluate(0, 2, PlatformVersion::latest()) + .unwrap(), + 0 + ); // At x = 3: (3 - 2)^2 = 1, f(3) = 2*1 + 10 = 12. - assert_eq!(distribution.evaluate(0, 3, PlatformVersion::latest()).unwrap(), 12); + assert_eq!( + distribution + .evaluate(0, 3, PlatformVersion::latest()) + .unwrap(), + 12 + ); } // Test: Non-zero offset o (shifting the base of the power) @@ -996,7 +1162,12 @@ 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, PlatformVersion::latest()).unwrap(), 42); + assert_eq!( + distribution + .evaluate(0, 1, PlatformVersion::latest()) + .unwrap(), + 42 + ); } // Test: Linear function when exponent is 1 (m = 1, n = 1) @@ -1014,7 +1185,12 @@ mod tests { max_value: None, }; // f(x) = 3*x + 5. At x = 10, f(10) = 30 + 5 = 35. - assert_eq!(distribution.evaluate(0, 10, PlatformVersion::latest()).unwrap(), 35); + assert_eq!( + distribution + .evaluate(0, 10, PlatformVersion::latest()) + .unwrap(), + 35 + ); } // Test: Cubic function (m = 3, n = 1) @@ -1032,7 +1208,12 @@ mod tests { max_value: None, }; // f(x) = x^3. At x = 4, f(4) = 64. - assert_eq!(distribution.evaluate(0, 4, PlatformVersion::latest()).unwrap(), 64); + assert_eq!( + distribution + .evaluate(0, 4, PlatformVersion::latest()) + .unwrap(), + 64 + ); } // Test: Combination of non-zero offset and shift @@ -1051,7 +1232,12 @@ 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, PlatformVersion::latest()).unwrap(), 16); + assert_eq!( + distribution + .evaluate(0, 3, PlatformVersion::latest()) + .unwrap(), + 16 + ); } } mod exp { @@ -1070,8 +1256,18 @@ mod tests { max_value: None, }; - assert_eq!(distribution.evaluate(0, 0, PlatformVersion::latest()).unwrap(), 11); - assert!(distribution.evaluate(0, 10, PlatformVersion::latest()).unwrap() > 20); + assert_eq!( + distribution + .evaluate(0, 0, PlatformVersion::latest()) + .unwrap(), + 11 + ); + assert!( + distribution + .evaluate(0, 10, PlatformVersion::latest()) + .unwrap() + > 20 + ); } #[test] @@ -1108,9 +1304,24 @@ mod tests { max_value: None, }; - assert_eq!(distribution.evaluate(0, 0, PlatformVersion::latest()).unwrap(), 7); - assert_eq!(distribution.evaluate(0, 5, PlatformVersion::latest()).unwrap(), 301); - assert_eq!(distribution.evaluate(0, 10, PlatformVersion::latest()).unwrap(), 44057); + assert_eq!( + distribution + .evaluate(0, 0, PlatformVersion::latest()) + .unwrap(), + 7 + ); + assert_eq!( + distribution + .evaluate(0, 5, PlatformVersion::latest()) + .unwrap(), + 301 + ); + assert_eq!( + distribution + .evaluate(0, 10, PlatformVersion::latest()) + .unwrap(), + 44057 + ); } #[test] @@ -1127,9 +1338,24 @@ mod tests { max_value: None, }; - assert_eq!(distribution.evaluate(0, 0, PlatformVersion::latest()).unwrap(), 0); - assert_eq!(distribution.evaluate(0, 50, PlatformVersion::latest()).unwrap(), 14); - assert_eq!(distribution.evaluate(0, 100, PlatformVersion::latest()).unwrap(), 2202); + assert_eq!( + distribution + .evaluate(0, 0, PlatformVersion::latest()) + .unwrap(), + 0 + ); + assert_eq!( + distribution + .evaluate(0, 50, PlatformVersion::latest()) + .unwrap(), + 14 + ); + assert_eq!( + distribution + .evaluate(0, 100, PlatformVersion::latest()) + .unwrap(), + 2202 + ); } #[test] @@ -1146,11 +1372,36 @@ mod tests { max_value: Some(100000000), }; - assert_eq!(distribution.evaluate(0, 0, PlatformVersion::latest()).unwrap(), 1); - assert_eq!(distribution.evaluate(0, 2, PlatformVersion::latest()).unwrap(), 2980); - assert_eq!(distribution.evaluate(0, 4, PlatformVersion::latest()).unwrap(), 8886110); - assert_eq!(distribution.evaluate(0, 10, PlatformVersion::latest()).unwrap(), 100000000); - assert_eq!(distribution.evaluate(0, 100000, PlatformVersion::latest()).unwrap(), 100000000); + assert_eq!( + distribution + .evaluate(0, 0, PlatformVersion::latest()) + .unwrap(), + 1 + ); + assert_eq!( + distribution + .evaluate(0, 2, PlatformVersion::latest()) + .unwrap(), + 2980 + ); + assert_eq!( + distribution + .evaluate(0, 4, PlatformVersion::latest()) + .unwrap(), + 8886110 + ); + assert_eq!( + distribution + .evaluate(0, 10, PlatformVersion::latest()) + .unwrap(), + 100000000 + ); + assert_eq!( + distribution + .evaluate(0, 100000, PlatformVersion::latest()) + .unwrap(), + 100000000 + ); } #[test] @@ -1167,9 +1418,24 @@ mod tests { max_value: None, }; - assert_eq!(distribution.evaluate(0, 0, PlatformVersion::latest()).unwrap(), 12); // f(0) = (2 * e^(-1 * (0 - 0 + 0) / 1)) / 1 + 10 - assert_eq!(distribution.evaluate(0, 5, PlatformVersion::latest()).unwrap(), 10); - assert_eq!(distribution.evaluate(0, 10000, PlatformVersion::latest()).unwrap(), 10); + assert_eq!( + distribution + .evaluate(0, 0, PlatformVersion::latest()) + .unwrap(), + 12 + ); // f(0) = (2 * e^(-1 * (0 - 0 + 0) / 1)) / 1 + 10 + assert_eq!( + distribution + .evaluate(0, 5, PlatformVersion::latest()) + .unwrap(), + 10 + ); + assert_eq!( + distribution + .evaluate(0, 10000, PlatformVersion::latest()) + .unwrap(), + 10 + ); } #[test] @@ -1186,9 +1452,24 @@ mod tests { max_value: None, }; - assert_eq!(distribution.evaluate(0, 0, PlatformVersion::latest()).unwrap(), 12); // f(0) = (2 * e^(-1 * (0 - 0 + 0) / 1)) / 1 + 10 - assert_eq!(distribution.evaluate(0, 5, PlatformVersion::latest()).unwrap(), 11); - assert_eq!(distribution.evaluate(0, 100, PlatformVersion::latest()).unwrap(), 11); + assert_eq!( + distribution + .evaluate(0, 0, PlatformVersion::latest()) + .unwrap(), + 12 + ); // f(0) = (2 * e^(-1 * (0 - 0 + 0) / 1)) / 1 + 10 + assert_eq!( + distribution + .evaluate(0, 5, PlatformVersion::latest()) + .unwrap(), + 11 + ); + assert_eq!( + distribution + .evaluate(0, 100, PlatformVersion::latest()) + .unwrap(), + 11 + ); } #[test] @@ -1206,12 +1487,16 @@ mod tests { }; assert_eq!( - distribution.evaluate(0, 0, PlatformVersion::latest()).unwrap(), + distribution + .evaluate(0, 0, PlatformVersion::latest()) + .unwrap(), 11, "Function should start at the max value" ); assert_eq!( - distribution.evaluate(0, 5, PlatformVersion::latest()).unwrap(), + distribution + .evaluate(0, 5, PlatformVersion::latest()) + .unwrap(), 11, "Function should be clamped at max value" ); @@ -1259,9 +1544,7 @@ mod tests { .dpp .token_versions .distribution_function_evaluate_version = 1; - let v1_result = distribution - .evaluate(0, 2, &deterministic_version) - .unwrap(); + let v1_result = distribution.evaluate(0, 2, &deterministic_version).unwrap(); // e^(-40) is extremely small but nonzero; result should be 0 after truncation assert_eq!(v1_result, 0); @@ -1299,8 +1582,18 @@ mod tests { max_value: None, }; - assert_eq!(distribution.evaluate(0, 1, PlatformVersion::latest()).unwrap(), 5); - assert!(distribution.evaluate(0, 10, PlatformVersion::latest()).unwrap() > 5); + assert_eq!( + distribution + .evaluate(0, 1, PlatformVersion::latest()) + .unwrap(), + 5 + ); + assert!( + distribution + .evaluate(0, 10, PlatformVersion::latest()) + .unwrap() + > 5 + ); } #[test] @@ -1317,8 +1610,18 @@ mod tests { max_value: Some(20), // Maximum bound should be enforced }; - assert_eq!(distribution.evaluate(0, 1, PlatformVersion::latest()).unwrap(), 7); // Clamped to min_value - assert!(distribution.evaluate(0, 10, PlatformVersion::latest()).unwrap() <= 20); // Should not exceed max_value + assert_eq!( + distribution + .evaluate(0, 1, PlatformVersion::latest()) + .unwrap(), + 7 + ); // Clamped to min_value + assert!( + distribution + .evaluate(0, 10, PlatformVersion::latest()) + .unwrap() + <= 20 + ); // Should not exceed max_value } #[test] @@ -1443,8 +1746,22 @@ mod tests { max_value: None, }; - assert!(distribution.evaluate(0, 1, PlatformVersion::latest()).unwrap() > distribution.evaluate(0, 5, PlatformVersion::latest()).unwrap()); - assert!(distribution.evaluate(0, 5, PlatformVersion::latest()).unwrap() > distribution.evaluate(0, 10, PlatformVersion::latest()).unwrap()); + assert!( + distribution + .evaluate(0, 1, PlatformVersion::latest()) + .unwrap() + > distribution + .evaluate(0, 5, PlatformVersion::latest()) + .unwrap() + ); + assert!( + distribution + .evaluate(0, 5, PlatformVersion::latest()) + .unwrap() + > distribution + .evaluate(0, 10, PlatformVersion::latest()) + .unwrap() + ); } #[test] @@ -1462,9 +1779,15 @@ mod tests { max_value: None, }; - let val1000 = distribution.evaluate(0, 1000, PlatformVersion::latest()).unwrap(); - let val2000 = distribution.evaluate(0, 2000, PlatformVersion::latest()).unwrap(); - let val3000 = distribution.evaluate(0, 3000, PlatformVersion::latest()).unwrap(); + let val1000 = distribution + .evaluate(0, 1000, PlatformVersion::latest()) + .unwrap(); + let val2000 = distribution + .evaluate(0, 2000, PlatformVersion::latest()) + .unwrap(); + let val3000 = distribution + .evaluate(0, 3000, PlatformVersion::latest()) + .unwrap(); assert!(val1000 < val2000, "Function should be increasing"); assert!(val2000 < val3000, "Function should be increasing"); @@ -1484,7 +1807,12 @@ mod tests { max_value: None, }; - assert_eq!(distribution.evaluate(0, 1, PlatformVersion::latest()).unwrap(), 0); // Should be clamped to 0 + assert_eq!( + distribution + .evaluate(0, 1, PlatformVersion::latest()) + .unwrap(), + 0 + ); // Should be clamped to 0 } #[test] @@ -1501,7 +1829,12 @@ mod tests { max_value: None, }; - assert_eq!(distribution.evaluate(0, 1000, PlatformVersion::latest()).unwrap(), 7); // Should be clamped to min_value + assert_eq!( + distribution + .evaluate(0, 1000, PlatformVersion::latest()) + .unwrap(), + 7 + ); // Should be clamped to min_value } #[test] @@ -1519,7 +1852,12 @@ mod tests { max_value: Some(20), }; - assert_eq!(distribution.evaluate(0, 500, PlatformVersion::latest()).unwrap(), 20); // Should be clamped to max_value + assert_eq!( + distribution + .evaluate(0, 500, PlatformVersion::latest()) + .unwrap(), + 20 + ); // Should be clamped to max_value } #[test] @@ -1597,12 +1935,16 @@ mod tests { }; assert_eq!( - distribution.evaluate(0, 0, PlatformVersion::latest()).unwrap(), + distribution + .evaluate(0, 0, PlatformVersion::latest()) + .unwrap(), 1, "Function should start at the max value" ); assert_eq!( - distribution.evaluate(0, 200, PlatformVersion::latest()).unwrap(), + distribution + .evaluate(0, 200, PlatformVersion::latest()) + .unwrap(), 10, "Function should remain clamped at max value" ); @@ -1623,7 +1965,9 @@ mod tests { }; assert_eq!( - distribution.evaluate(0, 1000, PlatformVersion::latest()).unwrap(), + distribution + .evaluate(0, 1000, PlatformVersion::latest()) + .unwrap(), 3, "Function should remain clamped at min value" ); @@ -1650,9 +1994,7 @@ mod tests { .dpp .token_versions .distribution_function_evaluate_version = 1; - let v1_result = distribution - .evaluate(0, 0, &deterministic_version) - .unwrap(); + let v1_result = distribution.evaluate(0, 0, &deterministic_version).unwrap(); assert_eq!(v1_result, 51); } } 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 29942129607..6001a64920d 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 @@ -1649,8 +1649,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(), platform_version)?; + 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 +1710,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, @@ -1839,8 +1843,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(), platform_version)?; + 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), 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 3e2883cda76..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, PlatformVersion::latest()).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, PlatformVersion::latest()).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, 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"); + 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, 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_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); From 52e667e841953beba89a2ead79071161622df1c4 Mon Sep 17 00:00:00 2001 From: pasta Date: Mon, 7 Sep 2026 22:59:26 -0500 Subject: [PATCH 04/10] build(dpp): pin libm to =0.2.16 The v1 evaluation path exists so that every node computes bit-identical rewards, and libm 0.2.x patch releases have changed pow/exp/log. A caret range lets a refreshed lockfile or a downstream consumer resolve a different implementation, re-creating the divergence through Cargo resolution instead of the OS libm. Cargo.lock already resolves 0.2.16, so this is a manifest-only change. Co-Authored-By: Claude Fable 5.1 --- packages/rs-dpp/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rs-dpp/Cargo.toml b/packages/rs-dpp/Cargo.toml index d8bb2ce7627..02f38fd065c 100644 --- a/packages/rs-dpp/Cargo.toml +++ b/packages/rs-dpp/Cargo.toml @@ -46,7 +46,7 @@ jsonschema = { git = "https://github.com/dashpay/jsonschema-rs", branch = "confi "draft202012", ], optional = true } lazy_static = { version = "1.4" } -libm = "0.2" +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"] } From dae9342c0824398cf4664d24b212d3558a292e67 Mon Sep 17 00:00:00 2001 From: pasta Date: Mon, 7 Sep 2026 23:00:30 -0500 Subject: [PATCH 05/10] refactor(dpp): resolve the evaluation version once per evaluate() call The 15-line version dispatch was copy-pasted into the Polynomial, Exponential, Logarithmic and InvertedLogarithmic arms, each with its own known_versions literal, and the integer-only variants never checked the version at all. Select a FloatOps { pow, exp, ln } table once at the top of evaluate() from a single KNOWN_EVALUATE_VERSIONS constant, so an unknown version is rejected uniformly for every variant and a future version is one edit. Co-Authored-By: Claude Fable 5.1 --- .../distribution_function/evaluate.rs | 111 ++++++++---------- 1 file changed, 49 insertions(+), 62 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 021926b434e..2bbbd029de2 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,10 +3,51 @@ 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 libm::{exp, log, pow}; 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`. +#[derive(Clone, Copy)] +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]; + +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 platform_version + .dpp + .token_versions + .distribution_function_evaluate_version + { + 0 => Ok(FloatOps { + pow: f64::powf, + exp: f64::exp, + ln: f64::ln, + }), + 1 => Ok(FloatOps { + pow: libm::pow, + exp: libm::exp, + ln: libm::log, + }), + version => Err(ProtocolError::UnknownVersionMismatch { + method: "DistributionFunction::evaluate".to_string(), + known_versions: KNOWN_EVALUATE_VERSIONS.to_vec(), + received: version, + }), + } + } +} + impl DistributionFunction { /// Evaluates the distribution function at the given period `x`. /// @@ -23,6 +64,9 @@ impl DistributionFunction { 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. @@ -225,21 +269,7 @@ impl DistributionFunction { )); } - let diff_exp = match platform_version - .dpp - .token_versions - .distribution_function_evaluate_version - { - 0 => (diff as f64).powf(exponent), - 1 => pow(diff as f64, exponent), - version => { - return Err(ProtocolError::UnknownVersionMismatch { - method: "DistributionFunction::evaluate (Polynomial)".to_string(), - known_versions: vec![0, 1], - received: version, - }) - } - }; + let diff_exp = (float_ops.pow)(diff as f64, exponent); if !diff_exp.is_finite() { return if diff_exp.is_sign_positive() { @@ -343,21 +373,7 @@ impl DistributionFunction { } let exponent = (*m as f64) * (diff as f64) / (*n as f64); - let exp_val = match platform_version - .dpp - .token_versions - .distribution_function_evaluate_version - { - 0 => exponent.exp(), - 1 => exp(exponent), - version => { - return Err(ProtocolError::UnknownVersionMismatch { - method: "DistributionFunction::evaluate (Exponential)".to_string(), - known_versions: vec![0, 1], - received: version, - }) - } - }; + 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 @@ -432,21 +448,7 @@ impl DistributionFunction { (*m as f64) * (diff as f64) / (*n as f64) }; - let log_val = match platform_version - .dpp - .token_versions - .distribution_function_evaluate_version - { - 0 => argument.ln(), - 1 => log(argument), - version => { - return Err(ProtocolError::UnknownVersionMismatch { - method: "DistributionFunction::evaluate (Logarithmic)".to_string(), - known_versions: vec![0, 1], - received: version, - }) - } - }; + 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) { @@ -584,22 +586,7 @@ impl DistributionFunction { )); } - let log_val = match platform_version - .dpp - .token_versions - .distribution_function_evaluate_version - { - 0 => argument.ln(), - 1 => log(argument), - version => { - return Err(ProtocolError::UnknownVersionMismatch { - method: "DistributionFunction::evaluate (InvertedLogarithmic)" - .to_string(), - known_versions: vec![0, 1], - received: version, - }) - } - }; + 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) { From 2454000bf56558295f2cadb747888a135de5e774 Mon Sep 17 00:00:00 2001 From: pasta Date: Mon, 7 Sep 2026 23:01:08 -0500 Subject: [PATCH 06/10] fix(dpp): label Logarithmic overflow errors as Logarithmic The Logarithmic arm's six overflow error strings said InvertedLogarithmic, a copy-paste from the real InvertedLogarithmic arm, so an overflow from either branch was indistinguishable in logs. Co-Authored-By: Claude Fable 5.1 --- .../distribution_function/evaluate.rs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 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 2bbbd029de2..c1ee6a2eb35 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 @@ -452,9 +452,7 @@ impl DistributionFunction { // 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 { @@ -472,12 +470,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 { @@ -485,20 +483,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", ))? }; From 4402499fa4ad9273984f6d615ddaa6dd1ff5a92e Mon Sep 17 00:00:00 2001 From: pasta Date: Mon, 7 Sep 2026 23:01:27 -0500 Subject: [PATCH 07/10] test(dpp): pin evaluate() tests to explicit versions and cover the unknown-version path Baseline tests now run under an explicit evaluation version 0 instead of PlatformVersion::latest(), so their expectations do not silently move when latest() does. Add a regression test that every DistributionFunction variant fails closed with UnknownVersionMismatch on an unknown version, and a fixture test evaluating the drive-abci block-based inverted-log and polynomial shapes under both v0 and v1 so the size of the std -> libm change is pinned rather than implicit. Rewrite the 125^(1/3) boundary test comment: the exponent rounds below 1/3, so the exact value is 4.99999999999999955... and a correctly-rounded pow returns 4.999999999999999 (truncating to 4); libm 0.2.16 returning exactly 5.0 is the implementation this test locks in as the consensus answer, not the mathematically closer one. The v0 result is only sanity-checked as 4 or 5 since it is platform-dependent by construction. Co-Authored-By: Claude Fable 5.1 --- .../distribution_function/evaluate.rs | 769 +++++++----------- 1 file changed, 306 insertions(+), 463 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 c1ee6a2eb35..d0f14533b47 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 @@ -654,28 +654,129 @@ mod tests { use super::*; use platform_version::version::PlatformVersion; use std::collections::BTreeMap; + use std::sync::LazyLock; + + /// `v0()` 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 test_fixed_amount() { - let distribution = DistributionFunction::FixedAmount { amount: 100 }; - assert_eq!( - distribution - .evaluate(0, 0, PlatformVersion::latest()) - .unwrap(), - 100 - ); - assert_eq!( + 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, + }, + ]; + + 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, 50, PlatformVersion::latest()) - .unwrap(), - 100 - ); - assert_eq!( + .evaluate(0, 5, v0()) + .expect("version 0 must evaluate"); distribution - .evaluate(0, 1000, PlatformVersion::latest()) - .unwrap(), - 100 - ); + .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, v0()).unwrap(), 100); + assert_eq!(distribution.evaluate(0, 50, v0()).unwrap(), 100); + assert_eq!(distribution.evaluate(0, 1000, v0()).unwrap(), 100); } #[test] @@ -686,42 +787,12 @@ mod tests { steps.insert(20, 25); let distribution = DistributionFunction::Stepwise(steps); - assert_eq!( - distribution - .evaluate(0, 0, PlatformVersion::latest()) - .unwrap(), - 100 - ); - assert_eq!( - distribution - .evaluate(0, 5, PlatformVersion::latest()) - .unwrap(), - 100 - ); - assert_eq!( - distribution - .evaluate(0, 10, PlatformVersion::latest()) - .unwrap(), - 50 - ); - assert_eq!( - distribution - .evaluate(0, 15, PlatformVersion::latest()) - .unwrap(), - 50 - ); - assert_eq!( - distribution - .evaluate(0, 20, PlatformVersion::latest()) - .unwrap(), - 25 - ); - assert_eq!( - distribution - .evaluate(0, 30, PlatformVersion::latest()) - .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] @@ -737,42 +808,12 @@ mod tests { min_value: Some(10), }; - assert_eq!( - distribution - .evaluate(0, 0, PlatformVersion::latest()) - .unwrap(), - 100 - ); - assert_eq!( - distribution - .evaluate(0, 9, PlatformVersion::latest()) - .unwrap(), - 100 - ); - assert_eq!( - distribution - .evaluate(0, 10, PlatformVersion::latest()) - .unwrap(), - 50 - ); - assert_eq!( - distribution - .evaluate(0, 20, PlatformVersion::latest()) - .unwrap(), - 25 - ); - assert_eq!( - distribution - .evaluate(0, 30, PlatformVersion::latest()) - .unwrap(), - 12 - ); - assert_eq!( - distribution - .evaluate(0, 40, PlatformVersion::latest()) - .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] @@ -789,7 +830,7 @@ mod tests { }; assert!(matches!( - distribution.evaluate(0, 10, PlatformVersion::latest()), + distribution.evaluate(0, 10, v0()), Err(ProtocolError::DivideByZero(_)) )); } @@ -801,9 +842,7 @@ mod tests { let distribution = DistributionFunction::Random { min: 10, max: 100 }; for x in 0..100 { - let result = distribution - .evaluate(0, x, PlatformVersion::latest()) - .unwrap(); + let result = distribution.evaluate(0, x, v0()).unwrap(); assert!( (10..=100).contains(&result), "Random value {} is out of range for x = {}", @@ -818,9 +857,7 @@ mod tests { let distribution = DistributionFunction::Random { min: 42, max: 42 }; for x in 0..10 { - let result = distribution - .evaluate(0, x, PlatformVersion::latest()) - .unwrap(); + let result = distribution.evaluate(0, x, v0()).unwrap(); assert_eq!( result, 42, "Expected fixed output 42, got {} for x = {}", @@ -833,7 +870,7 @@ mod tests { fn test_random_distribution_invalid_range() { let distribution = DistributionFunction::Random { min: 50, max: 40 }; - let result = distribution.evaluate(0, 0, PlatformVersion::latest()); + let result = distribution.evaluate(0, 0, v0()); assert!( matches!(result, Err(ProtocolError::Overflow(_))), "Expected ProtocolError::Overflow but got {:?}", @@ -845,12 +882,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, PlatformVersion::latest()) - .unwrap(); - let value2 = distribution - .evaluate(0, 42, PlatformVersion::latest()) - .unwrap(); + let value1 = distribution.evaluate(0, 42, v0()).unwrap(); + let value2 = distribution.evaluate(0, 42, v0()).unwrap(); assert_eq!( value1, value2, @@ -862,12 +895,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, PlatformVersion::latest()) - .unwrap(); - let value2 = distribution - .evaluate(0, 2, PlatformVersion::latest()) - .unwrap(); + let value1 = distribution.evaluate(0, 1, v0()).unwrap(); + let value2 = distribution.evaluate(0, 2, v0()).unwrap(); assert_ne!( value1, value2, @@ -888,30 +917,10 @@ mod tests { max_value: None, }; - assert_eq!( - distribution - .evaluate(0, 0, PlatformVersion::latest()) - .unwrap(), - 50 - ); - assert_eq!( - distribution - .evaluate(0, 2, PlatformVersion::latest()) - .unwrap(), - 60 - ); - assert_eq!( - distribution - .evaluate(0, 4, PlatformVersion::latest()) - .unwrap(), - 70 - ); - assert_eq!( - distribution - .evaluate(0, 6, PlatformVersion::latest()) - .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] @@ -925,24 +934,9 @@ mod tests { max_value: None, }; - assert_eq!( - distribution - .evaluate(0, 0, PlatformVersion::latest()) - .unwrap(), - 100 - ); - assert_eq!( - distribution - .evaluate(0, 10, PlatformVersion::latest()) - .unwrap(), - 50 - ); - assert_eq!( - distribution - .evaluate(0, 20, PlatformVersion::latest()) - .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] @@ -957,7 +951,7 @@ mod tests { }; assert!(matches!( - distribution.evaluate(0, 10, PlatformVersion::latest()), + distribution.evaluate(0, 10, v0()), Err(ProtocolError::DivideByZero(_)) )); } @@ -979,30 +973,10 @@ mod tests { max_value: None, }; - assert_eq!( - distribution - .evaluate(0, 0, PlatformVersion::latest()) - .unwrap(), - 0 - ); - assert_eq!( - distribution - .evaluate(0, 2, PlatformVersion::latest()) - .unwrap(), - 18 - ); - assert_eq!( - distribution - .evaluate(0, 3, PlatformVersion::latest()) - .unwrap(), - 28 - ); - assert_eq!( - distribution - .evaluate(0, 4, PlatformVersion::latest()) - .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] @@ -1020,7 +994,7 @@ mod tests { }; let result = distribution - .evaluate(0, 100000, PlatformVersion::latest()) + .evaluate(0, 100000, v0()) .expect("expected value"); assert_eq!(result, MAX_DISTRIBUTION_PARAM); } @@ -1040,12 +1014,7 @@ 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, PlatformVersion::latest()) - .unwrap(), - 8 - ); + assert_eq!(distribution.evaluate(0, 4, v0()).unwrap(), 8); } #[test] @@ -1062,22 +1031,104 @@ mod tests { max_value: None, }; - // cbrt(125) is exactly 5. The std f64 powf() on some platforms rounds - // the intermediate result below 5.0 and truncates to 4 when cast to u64. - // The deterministic libm path (version >= 1) must always return 5. - let mut deterministic_version = PlatformVersion::latest().clone(); - deterministic_version - .dpp - .token_versions - .distribution_function_evaluate_version = 1; - assert_eq!( - distribution - .evaluate(0, 125, &deterministic_version) - .unwrap(), - 5 + // 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}" ); } + /// Exact-value fixtures lifted from the drive-abci `inverted_logarithmic` and + /// polynomial block-based tests, evaluated under both versions, so the magnitude + /// of the v0 -> v1 change on real distribution shapes is pinned rather than + /// implicit. These all agree today (on every platform CI runs on); a fixture + /// that starts disagreeing is a divergence between std and libm worth knowing + /// about before activation. + #[test] + fn test_v0_and_v1_agree_on_block_based_fixtures() { + let fixtures: [(DistributionFunction, &[(u64, u64)]); 3] = [ + ( + 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)], + ), + ( + DistributionFunction::Polynomial { + a: 1, + d: 1, + m: 3, + n: 2, + o: 0, + start_moment: Some(0), + b: 0, + min_value: None, + max_value: None, + }, + &[(4, 8), (9, 27), (16, 64), (100, 1000)], + ), + ]; + + for (distribution, expectations) in fixtures { + for (x, expected) in expectations { + assert_eq!( + distribution.evaluate(0, *x, v1()).unwrap(), + *expected, + "v1 {distribution:?} at x={x}" + ); + assert_eq!( + distribution.evaluate(0, *x, v0()).unwrap(), + *expected, + "v0 {distribution:?} at x={x}" + ); + } + } + } + // Test: Negative coefficient a (should flip the sign) #[test] fn test_polynomial_function_negative_a() { @@ -1093,12 +1144,7 @@ mod tests { max_value: None, }; // f(x) = -1 * (x^2). For x = 3: -1 * (3^2) = -9. - assert_eq!( - distribution - .evaluate(0, 3, PlatformVersion::latest()) - .unwrap(), - 0 - ); + assert_eq!(distribution.evaluate(0, 3, v0()).unwrap(), 0); } // Test: Non-zero shift parameter s (shifting the x coordinate) @@ -1116,19 +1162,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, PlatformVersion::latest()) - .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, PlatformVersion::latest()) - .unwrap(), - 12 - ); + assert_eq!(distribution.evaluate(0, 3, v0()).unwrap(), 12); } // Test: Non-zero offset o (shifting the base of the power) @@ -1147,12 +1183,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, PlatformVersion::latest()) - .unwrap(), - 42 - ); + assert_eq!(distribution.evaluate(0, 1, v0()).unwrap(), 42); } // Test: Linear function when exponent is 1 (m = 1, n = 1) @@ -1170,12 +1201,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, PlatformVersion::latest()) - .unwrap(), - 35 - ); + assert_eq!(distribution.evaluate(0, 10, v0()).unwrap(), 35); } // Test: Cubic function (m = 3, n = 1) @@ -1193,12 +1219,7 @@ mod tests { max_value: None, }; // f(x) = x^3. At x = 4, f(4) = 64. - assert_eq!( - distribution - .evaluate(0, 4, PlatformVersion::latest()) - .unwrap(), - 64 - ); + assert_eq!(distribution.evaluate(0, 4, v0()).unwrap(), 64); } // Test: Combination of non-zero offset and shift @@ -1217,12 +1238,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, PlatformVersion::latest()) - .unwrap(), - 16 - ); + assert_eq!(distribution.evaluate(0, 3, v0()).unwrap(), 16); } } mod exp { @@ -1241,18 +1257,8 @@ mod tests { max_value: None, }; - assert_eq!( - distribution - .evaluate(0, 0, PlatformVersion::latest()) - .unwrap(), - 11 - ); - assert!( - distribution - .evaluate(0, 10, PlatformVersion::latest()) - .unwrap() - > 20 - ); + assert_eq!(distribution.evaluate(0, 0, v0()).unwrap(), 11); + assert!(distribution.evaluate(0, 10, v0()).unwrap() > 20); } #[test] @@ -1270,7 +1276,7 @@ mod tests { }; assert!(matches!( - distribution.evaluate(0, 10, PlatformVersion::latest()), + distribution.evaluate(0, 10, v0()), Err(ProtocolError::DivideByZero(_)) )); } @@ -1289,24 +1295,9 @@ mod tests { max_value: None, }; - assert_eq!( - distribution - .evaluate(0, 0, PlatformVersion::latest()) - .unwrap(), - 7 - ); - assert_eq!( - distribution - .evaluate(0, 5, PlatformVersion::latest()) - .unwrap(), - 301 - ); - assert_eq!( - distribution - .evaluate(0, 10, PlatformVersion::latest()) - .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] @@ -1323,24 +1314,9 @@ mod tests { max_value: None, }; - assert_eq!( - distribution - .evaluate(0, 0, PlatformVersion::latest()) - .unwrap(), - 0 - ); - assert_eq!( - distribution - .evaluate(0, 50, PlatformVersion::latest()) - .unwrap(), - 14 - ); - assert_eq!( - distribution - .evaluate(0, 100, PlatformVersion::latest()) - .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] @@ -1357,36 +1333,11 @@ mod tests { max_value: Some(100000000), }; - assert_eq!( - distribution - .evaluate(0, 0, PlatformVersion::latest()) - .unwrap(), - 1 - ); - assert_eq!( - distribution - .evaluate(0, 2, PlatformVersion::latest()) - .unwrap(), - 2980 - ); - assert_eq!( - distribution - .evaluate(0, 4, PlatformVersion::latest()) - .unwrap(), - 8886110 - ); - assert_eq!( - distribution - .evaluate(0, 10, PlatformVersion::latest()) - .unwrap(), - 100000000 - ); - assert_eq!( - distribution - .evaluate(0, 100000, PlatformVersion::latest()) - .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] @@ -1403,24 +1354,9 @@ mod tests { max_value: None, }; - assert_eq!( - distribution - .evaluate(0, 0, PlatformVersion::latest()) - .unwrap(), - 12 - ); // f(0) = (2 * e^(-1 * (0 - 0 + 0) / 1)) / 1 + 10 - assert_eq!( - distribution - .evaluate(0, 5, PlatformVersion::latest()) - .unwrap(), - 10 - ); - assert_eq!( - distribution - .evaluate(0, 10000, PlatformVersion::latest()) - .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] @@ -1437,24 +1373,9 @@ mod tests { max_value: None, }; - assert_eq!( - distribution - .evaluate(0, 0, PlatformVersion::latest()) - .unwrap(), - 12 - ); // f(0) = (2 * e^(-1 * (0 - 0 + 0) / 1)) / 1 + 10 - assert_eq!( - distribution - .evaluate(0, 5, PlatformVersion::latest()) - .unwrap(), - 11 - ); - assert_eq!( - distribution - .evaluate(0, 100, PlatformVersion::latest()) - .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] @@ -1472,16 +1393,12 @@ mod tests { }; assert_eq!( - distribution - .evaluate(0, 0, PlatformVersion::latest()) - .unwrap(), + distribution.evaluate(0, 0, v0()).unwrap(), 11, "Function should start at the max value" ); assert_eq!( - distribution - .evaluate(0, 5, PlatformVersion::latest()) - .unwrap(), + distribution.evaluate(0, 5, v0()).unwrap(), 11, "Function should be clamped at max value" ); @@ -1501,7 +1418,7 @@ mod tests { max_value: None, }; - let result = distribution.evaluate(0, 100000, PlatformVersion::latest()); + let result = distribution.evaluate(0, 100000, v0()); assert!( matches!(result, Err(ProtocolError::Overflow(_))), "Expected overflow but got {:?}", @@ -1524,12 +1441,7 @@ mod tests { }; // Verify the deterministic libm path produces a consistent result - let mut deterministic_version = PlatformVersion::latest().clone(); - deterministic_version - .dpp - .token_versions - .distribution_function_evaluate_version = 1; - let v1_result = distribution.evaluate(0, 2, &deterministic_version).unwrap(); + 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); @@ -1545,9 +1457,7 @@ mod tests { min_value: None, max_value: None, }; - let v1_result2 = distribution2 - .evaluate(0, 2, &deterministic_version) - .unwrap(); + let v1_result2 = distribution2.evaluate(0, 2, v1()).unwrap(); assert_eq!(v1_result2, 7); } } @@ -1567,18 +1477,8 @@ mod tests { max_value: None, }; - assert_eq!( - distribution - .evaluate(0, 1, PlatformVersion::latest()) - .unwrap(), - 5 - ); - assert!( - distribution - .evaluate(0, 10, PlatformVersion::latest()) - .unwrap() - > 5 - ); + assert_eq!(distribution.evaluate(0, 1, v0()).unwrap(), 5); + assert!(distribution.evaluate(0, 10, v0()).unwrap() > 5); } #[test] @@ -1595,18 +1495,8 @@ mod tests { max_value: Some(20), // Maximum bound should be enforced }; - assert_eq!( - distribution - .evaluate(0, 1, PlatformVersion::latest()) - .unwrap(), - 7 - ); // Clamped to min_value - assert!( - distribution - .evaluate(0, 10, PlatformVersion::latest()) - .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] @@ -1624,7 +1514,7 @@ mod tests { }; assert!(matches!( - distribution.evaluate(0, 1, PlatformVersion::latest()), + distribution.evaluate(0, 1, v0()), Err(ProtocolError::Overflow(_)) )); } @@ -1643,7 +1533,7 @@ mod tests { max_value: None, }; - let result = distribution.evaluate(0, 100, PlatformVersion::latest()); + let result = distribution.evaluate(0, 100, v0()); assert!(result.is_ok()); assert!(result.unwrap() > 10); // Function should increase over time } @@ -1663,7 +1553,7 @@ mod tests { }; assert!(matches!( - distribution.evaluate(0, 10, PlatformVersion::latest()), + distribution.evaluate(0, 10, v0()), Err(ProtocolError::DivideByZero(_)) )); } @@ -1683,7 +1573,7 @@ mod tests { }; assert!(matches!( - distribution.evaluate(0, 10, PlatformVersion::latest()), + distribution.evaluate(0, 10, v0()), Err(ProtocolError::DivideByZero(_)) )); } @@ -1704,14 +1594,7 @@ mod tests { max_value: None, }; - let mut deterministic_version = PlatformVersion::latest().clone(); - deterministic_version - .dpp - .token_versions - .distribution_function_evaluate_version = 1; - let v1_result = distribution - .evaluate(0, 100, &deterministic_version) - .unwrap(); + let v1_result = distribution.evaluate(0, 100, v1()).unwrap(); assert_eq!(v1_result, 46); } } @@ -1732,20 +1615,12 @@ mod tests { }; assert!( - distribution - .evaluate(0, 1, PlatformVersion::latest()) - .unwrap() - > distribution - .evaluate(0, 5, PlatformVersion::latest()) - .unwrap() + distribution.evaluate(0, 1, v0()).unwrap() + > distribution.evaluate(0, 5, v0()).unwrap() ); assert!( - distribution - .evaluate(0, 5, PlatformVersion::latest()) - .unwrap() - > distribution - .evaluate(0, 10, PlatformVersion::latest()) - .unwrap() + distribution.evaluate(0, 5, v0()).unwrap() + > distribution.evaluate(0, 10, v0()).unwrap() ); } @@ -1764,15 +1639,9 @@ mod tests { max_value: None, }; - let val1000 = distribution - .evaluate(0, 1000, PlatformVersion::latest()) - .unwrap(); - let val2000 = distribution - .evaluate(0, 2000, PlatformVersion::latest()) - .unwrap(); - let val3000 = distribution - .evaluate(0, 3000, PlatformVersion::latest()) - .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"); @@ -1792,12 +1661,7 @@ mod tests { max_value: None, }; - assert_eq!( - distribution - .evaluate(0, 1, PlatformVersion::latest()) - .unwrap(), - 0 - ); // Should be clamped to 0 + assert_eq!(distribution.evaluate(0, 1, v0()).unwrap(), 0); // Should be clamped to 0 } #[test] @@ -1814,12 +1678,7 @@ mod tests { max_value: None, }; - assert_eq!( - distribution - .evaluate(0, 1000, PlatformVersion::latest()) - .unwrap(), - 7 - ); // Should be clamped to min_value + assert_eq!(distribution.evaluate(0, 1000, v0()).unwrap(), 7); // Should be clamped to min_value } #[test] @@ -1837,12 +1696,7 @@ mod tests { max_value: Some(20), }; - assert_eq!( - distribution - .evaluate(0, 500, PlatformVersion::latest()) - .unwrap(), - 20 - ); // Should be clamped to max_value + assert_eq!(distribution.evaluate(0, 500, v0()).unwrap(), 20); // Should be clamped to max_value } #[test] @@ -1860,7 +1714,7 @@ mod tests { }; assert!(matches!( - distribution.evaluate(0, 1, PlatformVersion::latest()), + distribution.evaluate(0, 1, v0()), Err(ProtocolError::Overflow(_)) )); } @@ -1880,7 +1734,7 @@ mod tests { }; assert!(matches!( - distribution.evaluate(0, 10, PlatformVersion::latest()), + distribution.evaluate(0, 10, v0()), Err(ProtocolError::DivideByZero(_)) )); } @@ -1900,7 +1754,7 @@ mod tests { }; assert!(matches!( - distribution.evaluate(0, 10, PlatformVersion::latest()), + distribution.evaluate(0, 10, v0()), Err(ProtocolError::DivideByZero(_)) )); } @@ -1920,16 +1774,12 @@ mod tests { }; assert_eq!( - distribution - .evaluate(0, 0, PlatformVersion::latest()) - .unwrap(), + distribution.evaluate(0, 0, v0()).unwrap(), 1, "Function should start at the max value" ); assert_eq!( - distribution - .evaluate(0, 200, PlatformVersion::latest()) - .unwrap(), + distribution.evaluate(0, 200, v0()).unwrap(), 10, "Function should remain clamped at max value" ); @@ -1950,9 +1800,7 @@ mod tests { }; assert_eq!( - distribution - .evaluate(0, 1000, PlatformVersion::latest()) - .unwrap(), + distribution.evaluate(0, 1000, v0()).unwrap(), 3, "Function should remain clamped at min value" ); @@ -1974,12 +1822,7 @@ mod tests { max_value: None, }; - let mut deterministic_version = PlatformVersion::latest().clone(); - deterministic_version - .dpp - .token_versions - .distribution_function_evaluate_version = 1; - let v1_result = distribution.evaluate(0, 0, &deterministic_version).unwrap(); + let v1_result = distribution.evaluate(0, 0, v1()).unwrap(); assert_eq!(v1_result, 51); } } From 8d4afc838baeb873d7a4a242fb3c0a495435a020 Mon Sep 17 00:00:00 2001 From: pasta Date: Mon, 7 Sep 2026 23:01:45 -0500 Subject: [PATCH 08/10] feat(platform-version)!: activate deterministic reward evaluation at protocol v14 TOKEN_VERSIONS_V3 (distribution_function_evaluate_version 1) had no consumer, so the fix shipped inert: every reachable PlatformVersion, including latest(), still took the std powf/exp/ln path. Wire it into PLATFORM_V14, the next unreleased protocol version on v4.2-dev, document it as the sixth v14 consensus change, and add a test pinning v13 at version 0 and v14 at version 1 so the activation cannot be silently lost. Co-Authored-By: Claude Fable 5.1 --- .../dpp_versions/dpp_token_versions/v3.rs | 5 +-- .../rs-platform-version/src/version/v14.rs | 39 +++++++++++++++++-- 2 files changed, 38 insertions(+), 6 deletions(-) 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 index 374f595f765..166a94edda1 100644 --- 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 @@ -1,8 +1,7 @@ use crate::version::dpp_versions::dpp_token_versions::DPPTokenVersions; -/// NOTE: Not yet wired to any `PlatformVersion::PLATFORM_V*`. This constant sets -/// `distribution_function_evaluate_version: 1` (deterministic libm reward math), but -/// activation is deferred to a follow-up `PLATFORM_V13` PR. Until then it has no consumer. +/// 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, 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 + ); + } } From 09edb902db5fbef6d6f4d316b8af25966b79dbf8 Mon Sep 17 00:00:00 2001 From: pasta Date: Mon, 7 Sep 2026 23:37:14 -0500 Subject: [PATCH 09/10] test(dpp): tighten the evaluate() version tests Assert v0 == v1 on the inverted-log fixtures instead of pinning platform-dependent v0 values, drop the perfect-square Polynomial fixture that could never diverge, make the all-variants unknown-version test fail to compile when a variant is added, and remove an unused Copy derive on FloatOps. Co-Authored-By: Claude Fable 5.1 --- .../distribution_function/evaluate.rs | 152 +++++++++--------- 1 file changed, 74 insertions(+), 78 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 d0f14533b47..ea34595f21c 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 @@ -10,7 +10,6 @@ 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`. -#[derive(Clone, Copy)] struct FloatOps { pow: fn(f64, f64) -> f64, exp: fn(f64) -> f64, @@ -656,7 +655,7 @@ mod tests { use std::collections::BTreeMap; use std::sync::LazyLock; - /// `v0()` with `distribution_function_evaluate_version` forced to + /// `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(); @@ -750,6 +749,21 @@ mod tests { }, ]; + // 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!( @@ -1053,82 +1067,6 @@ mod tests { ); } - /// Exact-value fixtures lifted from the drive-abci `inverted_logarithmic` and - /// polynomial block-based tests, evaluated under both versions, so the magnitude - /// of the v0 -> v1 change on real distribution shapes is pinned rather than - /// implicit. These all agree today (on every platform CI runs on); a fixture - /// that starts disagreeing is a divergence between std and libm worth knowing - /// about before activation. - #[test] - fn test_v0_and_v1_agree_on_block_based_fixtures() { - let fixtures: [(DistributionFunction, &[(u64, u64)]); 3] = [ - ( - 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)], - ), - ( - DistributionFunction::Polynomial { - a: 1, - d: 1, - m: 3, - n: 2, - o: 0, - start_moment: Some(0), - b: 0, - min_value: None, - max_value: None, - }, - &[(4, 8), (9, 27), (16, 64), (100, 1000)], - ), - ]; - - for (distribution, expectations) in fixtures { - for (x, expected) in expectations { - assert_eq!( - distribution.evaluate(0, *x, v1()).unwrap(), - *expected, - "v1 {distribution:?} at x={x}" - ); - assert_eq!( - distribution.evaluate(0, *x, v0()).unwrap(), - *expected, - "v0 {distribution:?} at x={x}" - ); - } - } - } - // Test: Negative coefficient a (should flip the sign) #[test] fn test_polynomial_function_negative_a() { @@ -1825,5 +1763,63 @@ mod tests { 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}" + ); + } + } + } } } From 757b488c19f010e5a472cfde16f156136bf9eca0 Mon Sep 17 00:00:00 2001 From: pasta Date: Tue, 8 Sep 2026 00:08:20 -0500 Subject: [PATCH 10/10] fix(dpp): reject unknown evaluation versions before interval fast paths evaluate() rejects an unknown distribution_function_evaluate_version before dispatch, but evaluate_interval and evaluate_interval_with_explanation return Ok early for FixedAmount and for empty intervals without ever calling it, so an unsupported version succeeded or failed depending on distribution type and bounds. Factor the check into check_evaluate_version, used by FloatOps::for_version and now called at the entry of both interval methods before any early return. Test covers both fast paths through both entry points. Co-Authored-By: Claude Fable 5.1 --- .../distribution_function/evaluate.rs | 36 +++++++--- .../evaluate_interval.rs | 71 +++++++++++++++++++ 2 files changed, 96 insertions(+), 11 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 ea34595f21c..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 @@ -19,30 +19,44 @@ struct FloatOps { /// `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 platform_version - .dpp - .token_versions - .distribution_function_evaluate_version - { + match check_evaluate_version(platform_version)? { 0 => Ok(FloatOps { pow: f64::powf, exp: f64::exp, ln: f64::ln, }), - 1 => Ok(FloatOps { + _ => Ok(FloatOps { pow: libm::pow, exp: libm::exp, ln: libm::log, }), - version => Err(ProtocolError::UnknownVersionMismatch { - method: "DistributionFunction::evaluate".to_string(), - known_versions: KNOWN_EVALUATE_VERSIONS.to_vec(), - received: version, - }), } } } 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 6001a64920d..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 @@ -3,6 +3,7 @@ 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; @@ -1575,6 +1576,10 @@ impl DistributionFunction { 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)) @@ -1724,6 +1729,9 @@ impl DistributionFunction { 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, @@ -1904,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::*;