diff --git a/interface/src/emulated_u128.rs b/interface/src/emulated_u128.rs new file mode 100644 index 0000000..f23f2e1 --- /dev/null +++ b/interface/src/emulated_u128.rs @@ -0,0 +1,140 @@ +use core::cmp::Ordering; + +#[derive(Copy, Clone, Default, Debug, Eq, PartialEq)] +pub struct U128 { + pub hi: u64, + pub lo: u64, +} + +impl U128 { + pub const ZERO: Self = Self { hi: 0, lo: 0 }; + pub const MAX: Self = Self { + hi: u64::MAX, + lo: u64::MAX, + }; + + pub const fn from_u64(x: u64) -> Self { + Self { hi: 0, lo: x } + } + + pub const fn is_zero(self) -> bool { + self.hi == 0 && self.lo == 0 + } + + pub fn mul_u64(lhs: u64, rhs: u64) -> Self { + mul_u64_wide(lhs, rhs) + } + + pub fn checked_mul_u64(self, rhs: u64) -> Option { + if rhs == 0 || self.is_zero() { + return Some(Self::ZERO); + } + + // self * rhs = (self.lo * rhs) + (self.hi * rhs) << 64 + let lo_prod = mul_u64_wide(self.lo, rhs); // 128-bit + let hi_prod = mul_u64_wide(self.hi, rhs); // 128-bit + + // Shifting hi_prod left by 64 would discard hi_prod.hi beyond 128 -> overflow + if hi_prod.hi != 0 { + return None; + } + + // new_hi = lo_prod.hi + hi_prod.lo + // overflow => exceed 128 bits + let (new_hi, overflow) = lo_prod.hi.overflowing_add(hi_prod.lo); + if overflow { + return None; + } + + Some(Self { + hi: new_hi, + lo: lo_prod.lo, + }) + } + + pub fn saturating_mul_u64(self, rhs: u64) -> Self { + self.checked_mul_u64(rhs).unwrap_or(Self::MAX) + } + + /// Some magic copied from the internet + pub fn div_floor_u64_clamped(numer: Self, denom: Self, clamp: u64) -> u64 { + if clamp == 0 || numer.is_zero() || denom.is_zero() { + return 0; + } + if numer < denom { + return 0; + } + + if numer.hi == 0 && denom.hi == 0 { + return core::cmp::min(numer.lo / denom.lo, clamp); + } + + // Fast path: if denom*clamp <= numer, clamp + if let Some(prod) = denom.checked_mul_u64(clamp) { + if prod <= numer { + return clamp; + } + } + + let mut lo: u64 = 0; + let mut hi: u64 = clamp; + + for _ in 0..64 { + if lo == hi { + break; + } + + let diff = hi - lo; + let mid = lo + (diff / 2) + (diff & 1); + + let ok = match denom.checked_mul_u64(mid) { + Some(prod) => prod <= numer, + None => false, + }; + + if ok { + lo = mid; + } else { + hi = mid - 1; + } + } + + lo + } +} + +impl Ord for U128 { + fn cmp(&self, other: &Self) -> Ordering { + match self.hi.cmp(&other.hi) { + Ordering::Equal => self.lo.cmp(&other.lo), + ord => ord, + } + } +} + +impl PartialOrd for U128 { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +/// In order to multiply a u64*u64 you need to use 32-bit halves +fn mul_u64_wide(a: u64, b: u64) -> U128 { + const MASK32: u64 = 0xFFFF_FFFF; + + let a0 = a & MASK32; + let a1 = a >> 32; + let b0 = b & MASK32; + let b1 = b >> 32; + + let w0 = a0 * b0; // 64-bit + let t = a1 * b0 + (w0 >> 32); // fits in u64 + let w1 = t & MASK32; + let w2 = t >> 32; + + let t = a0 * b1 + w1; // fits in u64 + let lo = (t << 32) | (w0 & MASK32); + let hi = a1 * b1 + w2 + (t >> 32); + + U128 { hi, lo } +} diff --git a/interface/src/lib.rs b/interface/src/lib.rs index cbb9828..f3ddc7e 100644 --- a/interface/src/lib.rs +++ b/interface/src/lib.rs @@ -5,6 +5,7 @@ #[allow(deprecated)] pub mod config; +mod emulated_u128; pub mod error; pub mod instruction; pub mod stake_flags; diff --git a/interface/src/warmup_cooldown_allowance.rs b/interface/src/warmup_cooldown_allowance.rs index 95aa7af..0398163 100644 --- a/interface/src/warmup_cooldown_allowance.rs +++ b/interface/src/warmup_cooldown_allowance.rs @@ -1,4 +1,7 @@ -use {crate::stake_history::StakeHistoryEntry, solana_clock::Epoch}; +use { + crate::{emulated_u128::U128, stake_history::StakeHistoryEntry}, + solana_clock::Epoch, +}; pub const BASIS_POINTS_PER_UNIT: u64 = 10_000; pub const ORIGINAL_WARMUP_COOLDOWN_RATE_BPS: u64 = 2_500; // 25% @@ -79,17 +82,16 @@ fn rate_limited_stake_change( // If the multiplication would overflow, we saturate to u128::MAX. This ensures // that even in extreme edge cases, the rate-limiting invariant is maintained // (fail-safe) rather than bypassing rate limits entirely (fail-open). - let numerator = (account_portion as u128) - .saturating_mul(cluster_effective as u128) - .saturating_mul(rate_bps as u128); - let denominator = (cluster_portion as u128).saturating_mul(BASIS_POINTS_PER_UNIT as u128); + let numerator = U128::from_u64(account_portion) + .saturating_mul_u64(cluster_effective) + .saturating_mul_u64(rate_bps); + + let denominator = U128::mul_u64(cluster_portion, BASIS_POINTS_PER_UNIT); - // Safe unwrap as denominator cannot be zero due to early return guards above - let delta = numerator.checked_div(denominator).unwrap(); // The calculated delta can be larger than `account_portion` if the network's stake change // allowance is greater than the total stake waiting to change. In this case, the account's // entire portion is allowed to change. - delta.min(account_portion as u128) as u64 + U128::div_floor_u64_clamped(numerator, denominator, account_portion) } #[cfg(test)] @@ -382,9 +384,60 @@ mod test { (weight * newly_effective_cluster_stake) as u64 } + // Integer math implementation using native `u128`. + // Kept in tests as an oracle to ensure behavior is unchanged. + fn rate_limited_stake_change_native_u128( + epoch: Epoch, + account_portion: u64, + cluster_portion: u64, + cluster_effective: u64, + new_rate_activation_epoch: Option, + ) -> u64 { + if account_portion == 0 || cluster_portion == 0 || cluster_effective == 0 { + return 0; + } + + let rate_bps = warmup_cooldown_rate_bps(epoch, new_rate_activation_epoch); + + let numerator = (account_portion as u128) + .saturating_mul(cluster_effective as u128) + .saturating_mul(rate_bps as u128); + let denominator = (cluster_portion as u128).saturating_mul(BASIS_POINTS_PER_UNIT as u128); + + let delta = numerator.checked_div(denominator).unwrap(); + delta.min(account_portion as u128) as u64 + } + proptest! { #![proptest_config(ProptestConfig::with_cases(10_000))] + #[test] + fn rate_limited_change_matches_native_u128( + account_portion in 0u64..=u64::MAX, + cluster_portion in 0u64..=u64::MAX, + cluster_effective in 0u64..=u64::MAX, + current_epoch in 0u64..=2000, + new_rate_activation_epoch_option in prop::option::of(0u64..=2000), + ) { + let new_impl = rate_limited_stake_change( + current_epoch, + account_portion, + cluster_portion, + cluster_effective, + new_rate_activation_epoch_option, + ); + + let native_u128 = rate_limited_stake_change_native_u128( + current_epoch, + account_portion, + cluster_portion, + cluster_effective, + new_rate_activation_epoch_option, + ); + + prop_assert_eq!(new_impl, native_u128); + } + #[test] fn rate_limited_change_consistent_with_legacy( account_portion in 0u64..=u64::MAX,