Skip to content

Commit 45ccd8b

Browse files
committed
u128 emulation
1 parent 5f3fe65 commit 45ccd8b

File tree

3 files changed

+202
-8
lines changed

3 files changed

+202
-8
lines changed

interface/src/emulated_u128.rs

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
use core::cmp::Ordering;
2+
3+
#[derive(Copy, Clone, Default, Debug, Eq, PartialEq)]
4+
pub struct U128 {
5+
pub hi: u64,
6+
pub lo: u64,
7+
}
8+
9+
impl U128 {
10+
pub const ZERO: Self = Self { hi: 0, lo: 0 };
11+
pub const MAX: Self = Self {
12+
hi: u64::MAX,
13+
lo: u64::MAX,
14+
};
15+
16+
pub const fn from_u64(x: u64) -> Self {
17+
Self { hi: 0, lo: x }
18+
}
19+
20+
pub const fn is_zero(self) -> bool {
21+
self.hi == 0 && self.lo == 0
22+
}
23+
24+
pub fn mul_u64(lhs: u64, rhs: u64) -> Self {
25+
mul_u64_wide(lhs, rhs)
26+
}
27+
28+
pub fn checked_mul_u64(self, rhs: u64) -> Option<Self> {
29+
if rhs == 0 || self.is_zero() {
30+
return Some(Self::ZERO);
31+
}
32+
33+
// self * rhs = (self.lo * rhs) + (self.hi * rhs) << 64
34+
let lo_prod = mul_u64_wide(self.lo, rhs); // 128-bit
35+
let hi_prod = mul_u64_wide(self.hi, rhs); // 128-bit
36+
37+
// Shifting hi_prod left by 64 would discard hi_prod.hi beyond 128 -> overflow
38+
if hi_prod.hi != 0 {
39+
return None;
40+
}
41+
42+
// new_hi = lo_prod.hi + hi_prod.lo
43+
// overflow => exceed 128 bits
44+
let (new_hi, overflow) = lo_prod.hi.overflowing_add(hi_prod.lo);
45+
if overflow {
46+
return None;
47+
}
48+
49+
Some(Self {
50+
hi: new_hi,
51+
lo: lo_prod.lo,
52+
})
53+
}
54+
55+
pub fn saturating_mul_u64(self, rhs: u64) -> Self {
56+
self.checked_mul_u64(rhs).unwrap_or(Self::MAX)
57+
}
58+
59+
/// Some magic copied from the internet
60+
pub fn div_floor_u64_clamped(numer: Self, denom: Self, clamp: u64) -> u64 {
61+
if clamp == 0 || numer.is_zero() || denom.is_zero() {
62+
return 0;
63+
}
64+
if numer < denom {
65+
return 0;
66+
}
67+
68+
if numer.hi == 0 && denom.hi == 0 {
69+
return core::cmp::min(numer.lo / denom.lo, clamp);
70+
}
71+
72+
// Fast path: if denom*clamp <= numer, clamp
73+
if let Some(prod) = denom.checked_mul_u64(clamp) {
74+
if prod <= numer {
75+
return clamp;
76+
}
77+
}
78+
79+
let mut lo: u64 = 0;
80+
let mut hi: u64 = clamp;
81+
82+
for _ in 0..64 {
83+
if lo == hi {
84+
break;
85+
}
86+
87+
let diff = hi - lo;
88+
let mid = lo + (diff / 2) + (diff & 1);
89+
90+
let ok = match denom.checked_mul_u64(mid) {
91+
Some(prod) => prod <= numer,
92+
None => false,
93+
};
94+
95+
if ok {
96+
lo = mid;
97+
} else {
98+
hi = mid - 1;
99+
}
100+
}
101+
102+
lo
103+
}
104+
}
105+
106+
impl Ord for U128 {
107+
fn cmp(&self, other: &Self) -> Ordering {
108+
match self.hi.cmp(&other.hi) {
109+
Ordering::Equal => self.lo.cmp(&other.lo),
110+
ord => ord,
111+
}
112+
}
113+
}
114+
115+
impl PartialOrd for U128 {
116+
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
117+
Some(self.cmp(other))
118+
}
119+
}
120+
121+
/// In order to multiply a u64*u64 you need to use 32-bit halves
122+
fn mul_u64_wide(a: u64, b: u64) -> U128 {
123+
const MASK32: u64 = 0xFFFF_FFFF;
124+
125+
let a0 = a & MASK32;
126+
let a1 = a >> 32;
127+
let b0 = b & MASK32;
128+
let b1 = b >> 32;
129+
130+
let w0 = a0 * b0; // 64-bit
131+
let t = a1 * b0 + (w0 >> 32); // fits in u64
132+
let w1 = t & MASK32;
133+
let w2 = t >> 32;
134+
135+
let t = a0 * b1 + w1; // fits in u64
136+
let lo = (t << 32) | (w0 & MASK32);
137+
let hi = a1 * b1 + w2 + (t >> 32);
138+
139+
U128 { hi, lo }
140+
}

interface/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
#[allow(deprecated)]
77
pub mod config;
8+
mod emulated_u128;
89
pub mod error;
910
pub mod instruction;
1011
pub mod stake_flags;

interface/src/warmup_cooldown_allowance.rs

Lines changed: 61 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
use {crate::stake_history::StakeHistoryEntry, solana_clock::Epoch};
1+
use {
2+
crate::{emulated_u128::U128, stake_history::StakeHistoryEntry},
3+
solana_clock::Epoch,
4+
};
25

36
pub const BASIS_POINTS_PER_UNIT: u64 = 10_000;
47
pub const ORIGINAL_WARMUP_COOLDOWN_RATE_BPS: u64 = 2_500; // 25%
@@ -79,17 +82,16 @@ fn rate_limited_stake_change(
7982
// If the multiplication would overflow, we saturate to u128::MAX. This ensures
8083
// that even in extreme edge cases, the rate-limiting invariant is maintained
8184
// (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);
8690

87-
// Safe unwrap as denominator cannot be zero due to early return guards above
88-
let delta = numerator.checked_div(denominator).unwrap();
8991
// The calculated delta can be larger than `account_portion` if the network's stake change
9092
// allowance is greater than the total stake waiting to change. In this case, the account's
9193
// 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)
9395
}
9496

9597
#[cfg(test)]
@@ -382,9 +384,60 @@ mod test {
382384
(weight * newly_effective_cluster_stake) as u64
383385
}
384386

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+
385411
proptest! {
386412
#![proptest_config(ProptestConfig::with_cases(10_000))]
387413

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+
388441
#[test]
389442
fn rate_limited_change_consistent_with_legacy(
390443
account_portion in 0u64..=u64::MAX,

0 commit comments

Comments
 (0)