|
1 | | -use {crate::stake_history::StakeHistoryEntry, solana_clock::Epoch}; |
| 1 | +use { |
| 2 | + crate::{emulated_u128::U128, stake_history::StakeHistoryEntry}, |
| 3 | + solana_clock::Epoch, |
| 4 | +}; |
2 | 5 |
|
3 | 6 | pub const BASIS_POINTS_PER_UNIT: u64 = 10_000; |
4 | 7 | pub const ORIGINAL_WARMUP_COOLDOWN_RATE_BPS: u64 = 2_500; // 25% |
@@ -79,17 +82,16 @@ fn rate_limited_stake_change( |
79 | 82 | // If the multiplication would overflow, we saturate to u128::MAX. This ensures |
80 | 83 | // that even in extreme edge cases, the rate-limiting invariant is maintained |
81 | 84 | // (fail-safe) rather than bypassing rate limits entirely (fail-open). |
82 | | - let numerator = (account_portion as u128) |
83 | | - .saturating_mul(cluster_effective as u128) |
84 | | - .saturating_mul(rate_bps as u128); |
85 | | - let denominator = (cluster_portion as u128).saturating_mul(BASIS_POINTS_PER_UNIT as u128); |
| 85 | + let numerator = U128::from_u64(account_portion) |
| 86 | + .saturating_mul_u64(cluster_effective) |
| 87 | + .saturating_mul_u64(rate_bps); |
| 88 | + |
| 89 | + let denominator = U128::mul_u64(cluster_portion, BASIS_POINTS_PER_UNIT); |
86 | 90 |
|
87 | | - // Safe unwrap as denominator cannot be zero due to early return guards above |
88 | | - let delta = numerator.checked_div(denominator).unwrap(); |
89 | 91 | // The calculated delta can be larger than `account_portion` if the network's stake change |
90 | 92 | // allowance is greater than the total stake waiting to change. In this case, the account's |
91 | 93 | // entire portion is allowed to change. |
92 | | - delta.min(account_portion as u128) as u64 |
| 94 | + U128::div_floor_u64_clamped(numerator, denominator, account_portion) |
93 | 95 | } |
94 | 96 |
|
95 | 97 | #[cfg(test)] |
@@ -382,9 +384,60 @@ mod test { |
382 | 384 | (weight * newly_effective_cluster_stake) as u64 |
383 | 385 | } |
384 | 386 |
|
| 387 | + // Integer math implementation using native `u128`. |
| 388 | + // Kept in tests as an oracle to ensure behavior is unchanged. |
| 389 | + fn rate_limited_stake_change_native_u128( |
| 390 | + epoch: Epoch, |
| 391 | + account_portion: u64, |
| 392 | + cluster_portion: u64, |
| 393 | + cluster_effective: u64, |
| 394 | + new_rate_activation_epoch: Option<Epoch>, |
| 395 | + ) -> u64 { |
| 396 | + if account_portion == 0 || cluster_portion == 0 || cluster_effective == 0 { |
| 397 | + return 0; |
| 398 | + } |
| 399 | + |
| 400 | + let rate_bps = warmup_cooldown_rate_bps(epoch, new_rate_activation_epoch); |
| 401 | + |
| 402 | + let numerator = (account_portion as u128) |
| 403 | + .saturating_mul(cluster_effective as u128) |
| 404 | + .saturating_mul(rate_bps as u128); |
| 405 | + let denominator = (cluster_portion as u128).saturating_mul(BASIS_POINTS_PER_UNIT as u128); |
| 406 | + |
| 407 | + let delta = numerator.checked_div(denominator).unwrap(); |
| 408 | + delta.min(account_portion as u128) as u64 |
| 409 | + } |
| 410 | + |
385 | 411 | proptest! { |
386 | 412 | #![proptest_config(ProptestConfig::with_cases(10_000))] |
387 | 413 |
|
| 414 | + #[test] |
| 415 | + fn rate_limited_change_matches_native_u128( |
| 416 | + account_portion in 0u64..=u64::MAX, |
| 417 | + cluster_portion in 0u64..=u64::MAX, |
| 418 | + cluster_effective in 0u64..=u64::MAX, |
| 419 | + current_epoch in 0u64..=2000, |
| 420 | + new_rate_activation_epoch_option in prop::option::of(0u64..=2000), |
| 421 | + ) { |
| 422 | + let new_impl = rate_limited_stake_change( |
| 423 | + current_epoch, |
| 424 | + account_portion, |
| 425 | + cluster_portion, |
| 426 | + cluster_effective, |
| 427 | + new_rate_activation_epoch_option, |
| 428 | + ); |
| 429 | + |
| 430 | + let native_u128 = rate_limited_stake_change_native_u128( |
| 431 | + current_epoch, |
| 432 | + account_portion, |
| 433 | + cluster_portion, |
| 434 | + cluster_effective, |
| 435 | + new_rate_activation_epoch_option, |
| 436 | + ); |
| 437 | + |
| 438 | + prop_assert_eq!(new_impl, native_u128); |
| 439 | + } |
| 440 | + |
388 | 441 | #[test] |
389 | 442 | fn rate_limited_change_consistent_with_legacy( |
390 | 443 | account_portion in 0u64..=u64::MAX, |
|
0 commit comments