hopper_native/sysvar.rs
1//! Sysvar access via direct syscalls.
2//!
3//! Provides zero-alloc, zero-deserialization access to Solana sysvars
4//! by reading them directly into stack buffers via syscalls, including the
5//! epoch schedule sysvar.
6
7use crate::address::Address;
8use crate::error::ProgramError;
9
10// ── Clock ────────────────────────────────────────────────────────────
11
12/// Clock sysvar data, read directly from the runtime.
13#[repr(C)]
14#[derive(Clone, Copy, Debug, Default)]
15pub struct Clock {
16 pub slot: u64,
17 pub epoch_start_timestamp: i64,
18 pub epoch: u64,
19 pub leader_schedule_epoch: u64,
20 pub unix_timestamp: i64,
21}
22
23/// Read the Clock sysvar.
24#[inline]
25pub fn get_clock() -> Result<Clock, ProgramError> {
26 #[allow(unused_mut)]
27 let mut clock = Clock::default();
28
29 #[cfg(target_os = "solana")]
30 {
31 let rc =
32 // SAFETY: This block is part of Hopper's reviewed zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
33 unsafe { crate::syscalls::sol_get_clock_sysvar(&mut clock as *mut Clock as *mut u8) };
34 if rc != 0 {
35 return Err(ProgramError::UnsupportedSysvar);
36 }
37 }
38
39 Ok(clock)
40}
41
42impl Clock {
43 /// Read the Clock sysvar.
44 ///
45 /// Method-style alias for [`get_clock`], matching the `Sysvar::get()`
46 /// ergonomics other Solana frameworks expose (`Clock::get()`).
47 #[inline]
48 pub fn get() -> Result<Self, ProgramError> {
49 get_clock()
50 }
51}
52
53// ── Rent ─────────────────────────────────────────────────────────────
54
55/// Rent sysvar data.
56#[repr(C)]
57#[derive(Clone, Copy, Debug, Default)]
58pub struct Rent {
59 pub lamports_per_byte_year: u64,
60 pub exemption_threshold: f64,
61 pub burn_percent: u8,
62}
63
64/// Lamports charged per byte of account storage per year.
65///
66/// This is the launch-era value baked into Solana's original rent config. It
67/// is a historical snapshot of a runtime-owned parameter; Mainnet now carries
68/// the effective per-byte rate directly in the live [`Rent`] sysvar.
69/// Reaping-relevant decisions must read that sysvar (see
70/// [`Rent::minimum_balance`]), not this constant.
71pub const LAMPORTS_PER_BYTE_YEAR: u64 = 3_480;
72
73/// Years of rent an account must prepay to be rent-exempt.
74///
75/// Launch-era snapshot of the runtime's `exemption_threshold`. SIMD-0194
76/// deprecated the field and Mainnet now stores `1.0`, while the effective
77/// per-byte rate carries the complete price. The field remains in the wire
78/// layout for compatibility.
79pub const EXEMPTION_THRESHOLD_YEARS: u64 = 2;
80
81/// Fixed per-account storage overhead charged by the cluster.
82pub const ACCOUNT_STORAGE_OVERHEAD: u64 = 128;
83
84/// Minimum balance for rent exemption **assuming the launch-era rent
85/// constants** ([`LAMPORTS_PER_BYTE_YEAR`], [`EXEMPTION_THRESHOLD_YEARS`],
86/// [`ACCOUNT_STORAGE_OVERHEAD`]).
87///
88/// This is the fast, allocation-free, syscall-free path: pure `const`
89/// integer arithmetic over hardcoded constants. It is exact **for a cluster
90/// running that legacy config** and byte-matches [`Rent::minimum_balance`]
91/// when the live sysvar carries those same constants.
92///
93/// # SAFETY-CRITICAL caveat, do NOT gate reaping on this
94///
95/// Because the constants are hardcoded, this function cannot see a rent
96/// *reprice*. It can overcharge after a reduction or under-fund after a later
97/// increase. Any code path that decides whether an account is safe from
98/// reaping, topping an account up to exemption, or gating a resize on it,
99/// must therefore use the live value.
100///
101/// For those paths read the live [`Rent`] sysvar and call
102/// [`Rent::minimum_balance`] (see [`crate::batch::require_rent_exempt_with`]
103/// and [`crate::batch::realloc_checked_with`]). Keep this const form only
104/// where a fixed legacy snapshot is explicitly intended.
105#[inline]
106pub const fn rent_exempt_minimum(data_len: usize) -> u64 {
107 (data_len as u64 + ACCOUNT_STORAGE_OVERHEAD)
108 * LAMPORTS_PER_BYTE_YEAR
109 * EXEMPTION_THRESHOLD_YEARS
110}
111
112/// Read the Rent sysvar.
113#[inline]
114pub fn get_rent() -> Result<Rent, ProgramError> {
115 #[allow(unused_mut)]
116 let mut rent = Rent::default();
117
118 #[cfg(target_os = "solana")]
119 {
120 // SAFETY: This block is part of Hopper's reviewed zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
121 let rc = unsafe { crate::syscalls::sol_get_rent_sysvar(&mut rent as *mut Rent as *mut u8) };
122 if rc != 0 {
123 return Err(ProgramError::UnsupportedSysvar);
124 }
125 }
126
127 Ok(rent)
128}
129
130impl Rent {
131 /// Read the Rent sysvar.
132 ///
133 /// Method-style alias for [`get_rent`], matching the `Sysvar::get()`
134 /// ergonomics other Solana frameworks expose (`Rent::get()`).
135 #[inline]
136 pub fn get() -> Result<Self, ProgramError> {
137 get_rent()
138 }
139
140 /// Minimum lamports for rent exemption at `data_len`, computed from the
141 /// **live sysvar** values, the correct source for reaping-relevant
142 /// decisions after a rent reprice.
143 ///
144 /// This follows Solana's own `solana_rent::Rent::minimum_balance` for the
145 /// two thresholds any cluster has stored, with no floating point at all:
146 ///
147 /// ```text
148 /// integer_part = (ACCOUNT_STORAGE_OVERHEAD + data_len) * rate
149 /// threshold 1.0 => integer_part
150 /// threshold 2.0 => integer_part * 2
151 /// otherwise => integer_part * ceil(threshold) (never below Solana's
152 /// `(integer_part as f64 * threshold) as u64`)
153 /// ```
154 ///
155 /// SIMD-0194 made `1.0` the live wire marker and moved the full price into
156 /// the rate field; `2.0` is the launch-era value. Both are matched by bit
157 /// pattern (see [`scale_by_exemption_threshold`]), so a program that reads
158 /// the sysvar links no soft-float code. A threshold no cluster has ever
159 /// used rounds up to whole years, which can only overfund.
160 ///
161 /// The integer product uses saturating ops purely as an overflow guard;
162 /// for every loader-permitted `data_len` (`<= 10_485_760`) and realistic
163 /// `lamports_per_byte_year` it never saturates, so the byte-match with the
164 /// runtime is exact (proven in the Kani harnesses below).
165 #[inline]
166 pub fn minimum_balance(&self, data_len: usize) -> u64 {
167 // The same product Solana's `Rent::minimum_balance` computes, which
168 // does not saturate either: the loader caps `data_len` at 10 MiB, so
169 // the byte term is below 2^24 and the product below 2^64 for any
170 // rate under 2^40 lamports per byte-year. `saturating_mul` here
171 // linked and called the 128-bit `__multi3` helper (344 bytes, about
172 // 50 CU) on every `init` (measured 2026-09-21).
173 let bytes = data_len as u64;
174 let integer_part = ACCOUNT_STORAGE_OVERHEAD
175 .saturating_add(bytes)
176 .wrapping_mul(self.lamports_per_byte_year);
177 scale_by_exemption_threshold(integer_part, self.exemption_threshold.to_bits())
178 }
179}
180
181/// `a * b`, saturating at `u64::MAX`, without the 128-bit multiply helper.
182///
183/// `u64::saturating_mul` and `checked_mul` lower to `umul.with.overflow`,
184/// which SBF has no instruction for, so LLVM links `__multi3` (344 bytes)
185/// and calls it (about 50 CU); the `a > u64::MAX / b` and `(a * b) / b != a`
186/// guards are recognized as the same idiom and get the same helper.
187/// Splitting both operands into 32-bit halves decides overflow with 64-bit
188/// arithmetic only: the product exceeds 64 bits exactly when both high
189/// halves are nonzero, when the cross term reaches 2^32, or when the final
190/// add carries.
191#[inline(always)]
192pub const fn saturating_mul_u64(a: u64, b: u64) -> u64 {
193 let (ah, al) = (a >> 32, a & 0xFFFF_FFFF);
194 let (bh, bl) = (b >> 32, b & 0xFFFF_FFFF);
195 if ah != 0 && bh != 0 {
196 return u64::MAX;
197 }
198 // At most one high half is nonzero, so each term is a 32 x 32 product
199 // and one addend is zero: exact in 64 bits.
200 let cross = ah * bl + al * bh;
201 if cross >> 32 != 0 {
202 return u64::MAX;
203 }
204 match (cross << 32).checked_add(al * bl) {
205 Some(product) => product,
206 None => u64::MAX,
207 }
208}
209
210/// Bit pattern of `1.0f64`, the SIMD-0194 live threshold marker.
211const THRESHOLD_ONE_BITS: u64 = 0x3FF0_0000_0000_0000;
212/// Bit pattern of `2.0f64`, the launch-era threshold.
213const THRESHOLD_TWO_BITS: u64 = 0x4000_0000_0000_0000;
214
215/// Apply the rent `exemption_threshold` (given as its IEEE-754 bit pattern)
216/// to an integer lamport amount **without any floating-point instruction**.
217///
218/// sBPF has no FPU: every `f64` compare or multiply lowers to a soft-float
219/// library call, and the launch-era formula's `(x as f64 * t) as u64`
220/// fallback dragged `__muldf3`, `__floatundidf`, and `__fixunsdfdi` into
221/// every program that read the Rent sysvar (measured 2026-09-21 on the
222/// framework-comparison counter: 2,528 bytes of `.text` for a path no
223/// cluster has ever taken). The two thresholds that have existed are
224/// matched by bit pattern and are exact: `1.0`, the SIMD-0194 wire marker
225/// every public cluster stores today, and the launch-era `2.0`. Any other
226/// finite positive threshold is rounded **up** to a whole number of years
227/// with integer arithmetic, which can only overfund, never underfund, so a
228/// rent-exemption decision made through it stays safe. Saturates at
229/// `u64::MAX` (an infinite threshold saturates too); a zero, negative, or
230/// NaN threshold yields `0`, the same as the cast.
231#[inline]
232pub fn scale_by_exemption_threshold(integer_part: u64, threshold_bits: u64) -> u64 {
233 if threshold_bits == THRESHOLD_ONE_BITS {
234 return integer_part;
235 }
236 if threshold_bits == THRESHOLD_TWO_BITS {
237 return integer_part.saturating_mul(2);
238 }
239 saturating_mul_u64(integer_part, ceil_years(threshold_bits))
240}
241
242/// Ceiling of a double (given as bits) as a `u64`, saturating: the whole
243/// number of years a non-standard threshold rounds up to. Zero, negative,
244/// and NaN give `0`; anything in `(0, 1]` gives `1`; infinity saturates.
245#[inline]
246fn ceil_years(bits: u64) -> u64 {
247 if bits >> 63 == 1 {
248 return 0;
249 }
250 let exponent = ((bits >> 52) & 0x7FF) as i32;
251 let mantissa = bits & ((1u64 << 52) - 1);
252 if exponent == 0x7FF {
253 return if mantissa == 0 { u64::MAX } else { 0 };
254 }
255 if exponent == 0 {
256 // Zero, or a subnormal that still rounds up to one year.
257 return if mantissa == 0 { 0 } else { 1 };
258 }
259 // value = 1.mantissa * 2^(exponent - 1023)
260 let e = exponent - 1023;
261 if e < 0 {
262 return 1;
263 }
264 if e >= 64 {
265 return u64::MAX;
266 }
267 let significand = (1u64 << 52) | mantissa;
268 if e >= 52 {
269 return significand << (e - 52);
270 }
271 let shift = (52 - e) as u32;
272 let whole = significand >> shift;
273 let fraction = significand & ((1u64 << shift) - 1);
274 if fraction == 0 {
275 whole
276 } else {
277 whole + 1
278 }
279}
280
281// ── Epoch Schedule ───────────────────────────────────────────────────
282
283/// Epoch schedule sysvar data.
284///
285/// Nobody wraps this at the native level. Useful for programs that
286/// need to reason about epoch boundaries (staking, vesting, time locks).
287#[repr(C)]
288#[derive(Clone, Copy, Debug, Default)]
289pub struct EpochSchedule {
290 pub slots_per_epoch: u64,
291 pub leader_schedule_slot_offset: u64,
292 pub warmup: bool,
293 pub first_normal_epoch: u64,
294 pub first_normal_slot: u64,
295}
296
297// ABI lock: `sol_get_epoch_schedule_sysvar` memcpy's the runtime's
298// `#[repr(C)]` `EpochSchedule` into this buffer. The canonical Agave
299// definition (solana-sdk `epoch-schedule`) is `#[repr(C)]` with field
300// order `slots_per_epoch, leader_schedule_slot_offset, warmup,
301// first_normal_epoch, first_normal_slot`. The `bool` sits between two
302// u64 fields, so the layout depends on `repr(C)` padding, any drift in
303// field order or repr here silently misreads every field after `warmup`.
304// These asserts fail the build if that ever happens.
305const _: () = {
306 assert!(core::mem::size_of::<EpochSchedule>() == 40);
307 assert!(core::mem::align_of::<EpochSchedule>() == 8);
308 assert!(core::mem::offset_of!(EpochSchedule, slots_per_epoch) == 0);
309 assert!(core::mem::offset_of!(EpochSchedule, leader_schedule_slot_offset) == 8);
310 assert!(core::mem::offset_of!(EpochSchedule, warmup) == 16);
311 assert!(core::mem::offset_of!(EpochSchedule, first_normal_epoch) == 24);
312 assert!(core::mem::offset_of!(EpochSchedule, first_normal_slot) == 32);
313};
314
315// The Clock sysvar is also memcpy'd from a `#[repr(C)]` runtime struct.
316const _: () = {
317 assert!(core::mem::size_of::<Clock>() == 40);
318 assert!(core::mem::offset_of!(Clock, slot) == 0);
319 assert!(core::mem::offset_of!(Clock, epoch_start_timestamp) == 8);
320 assert!(core::mem::offset_of!(Clock, epoch) == 16);
321 assert!(core::mem::offset_of!(Clock, leader_schedule_epoch) == 24);
322 assert!(core::mem::offset_of!(Clock, unix_timestamp) == 32);
323};
324
325/// Read the EpochSchedule sysvar.
326#[inline]
327pub fn get_epoch_schedule() -> Result<EpochSchedule, ProgramError> {
328 #[allow(unused_mut)]
329 let mut schedule = EpochSchedule::default();
330
331 #[cfg(target_os = "solana")]
332 {
333 // SAFETY: This block is part of Hopper's reviewed zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
334 let rc = unsafe {
335 crate::syscalls::sol_get_epoch_schedule_sysvar(
336 &mut schedule as *mut EpochSchedule as *mut u8,
337 )
338 };
339 if rc != 0 {
340 return Err(ProgramError::UnsupportedSysvar);
341 }
342 }
343
344 Ok(schedule)
345}
346
347impl EpochSchedule {
348 /// Get the epoch for a given slot.
349 #[inline]
350 pub fn get_epoch(&self, slot: u64) -> u64 {
351 if slot < self.first_normal_slot {
352 // During warmup, epoch length doubles each epoch.
353 // Initial epoch has 32 slots (MINIMUM_SLOTS_PER_EPOCH).
354 if slot == 0 {
355 return 0;
356 }
357 // log2(slot / 32) + 1, clamped.
358 let mut epoch_len: u64 = 32; // MINIMUM_SLOTS_PER_EPOCH
359 let mut epoch: u64 = 0;
360 let mut slot_remaining = slot;
361 while slot_remaining >= epoch_len {
362 slot_remaining -= epoch_len;
363 epoch += 1;
364 epoch_len = epoch_len.saturating_mul(2);
365 }
366 epoch
367 } else {
368 let normal_slot_index = slot - self.first_normal_slot;
369 self.first_normal_epoch + normal_slot_index / self.slots_per_epoch
370 }
371 }
372
373 /// Get the first slot in the given epoch.
374 #[inline]
375 pub fn get_first_slot_in_epoch(&self, epoch: u64) -> u64 {
376 if epoch <= self.first_normal_epoch {
377 // Warmup: each epoch doubles in length starting from 32.
378 if epoch == 0 {
379 return 0;
380 }
381 // First slot = sum of all previous epoch lengths.
382 // = 32 * (2^epoch - 1)
383 let shift = epoch.min(63);
384 32_u64.saturating_mul((1_u64 << shift).saturating_sub(1))
385 } else {
386 let normal_epoch_index = epoch - self.first_normal_epoch;
387 self.first_normal_slot + normal_epoch_index * self.slots_per_epoch
388 }
389 }
390}
391
392// ── Well-known sysvar addresses ──────────────────────────────────────
393
394/// Clock sysvar address.
395pub const CLOCK_ID: Address = crate::address!("SysvarC1ock11111111111111111111111111111111");
396
397/// Rent sysvar address.
398pub const RENT_ID: Address = crate::address!("SysvarRent111111111111111111111111111111111");
399
400/// Epoch schedule sysvar address.
401pub const EPOCH_SCHEDULE_ID: Address =
402 crate::address!("SysvarEpochSchedu1e111111111111111111111111");
403
404/// SlotHashes sysvar address.
405pub const SLOT_HASHES_ID: Address = crate::address!("SysvarS1otHashes111111111111111111111111111");
406
407/// StakeHistory sysvar address.
408pub const STAKE_HISTORY_ID: Address =
409 crate::address!("SysvarStakeHistory1111111111111111111111111");
410
411/// Instructions sysvar address (for instruction introspection).
412pub const INSTRUCTIONS_ID: Address = crate::address!("Sysvar1nstructions1111111111111111111111111");
413
414/// EpochRewards sysvar address (SIMD-0118).
415pub const EPOCH_REWARDS_ID: Address =
416 crate::address!("SysvarEpochRewards1111111111111111111111111");
417
418// ── Generalized sysvar access (sol_get_sysvar) ──────────────────────
419
420/// Copy `dst.len()` bytes starting at `offset` from the sysvar identified
421/// by `sysvar_id` into `dst`.
422///
423/// This wraps the modern `sol_get_sysvar` syscall, the only zero-copy way
424/// to read large sysvars (SlotHashes, StakeHistory) without passing them
425/// as instruction accounts. Returns `Err(UnsupportedSysvar)` on syscall
426/// failure (e.g. reading past the sysvar's length).
427#[inline]
428pub fn get_sysvar_into(
429 sysvar_id: &Address,
430 offset: u64,
431 dst: &mut [u8],
432) -> Result<(), ProgramError> {
433 #[cfg(target_os = "solana")]
434 {
435 // SAFETY: `sysvar_id` is a 32-byte address; `dst` is valid for its
436 // own length; the syscall copies exactly `dst.len()` bytes.
437 let rc = unsafe {
438 crate::syscalls::sol_get_sysvar(
439 sysvar_id.as_array().as_ptr(),
440 dst.as_mut_ptr(),
441 offset,
442 dst.len() as u64,
443 )
444 };
445 if rc != 0 {
446 return Err(ProgramError::UnsupportedSysvar);
447 }
448 }
449 #[cfg(not(target_os = "solana"))]
450 {
451 let _ = (sysvar_id, offset, dst);
452 }
453 Ok(())
454}
455
456// ── Epoch stake (sol_get_epoch_stake, SIMD-0133) ────────────────────
457
458/// Get the current-epoch activated stake of the vote account at `vote`.
459#[inline]
460pub fn get_epoch_stake(vote: &Address) -> u64 {
461 #[cfg(target_os = "solana")]
462 {
463 // SAFETY: `vote` is a 32-byte address pointer the syscall reads.
464 unsafe { crate::syscalls::sol_get_epoch_stake(vote.as_array().as_ptr()) }
465 }
466 #[cfg(not(target_os = "solana"))]
467 {
468 let _ = vote;
469 0
470 }
471}
472
473/// Get the cluster-wide total activated stake for the current epoch.
474#[inline]
475pub fn get_total_epoch_stake() -> u64 {
476 #[cfg(target_os = "solana")]
477 {
478 // SAFETY: a null `vote_address` is the documented request for the
479 // cluster total (SIMD-0133).
480 unsafe { crate::syscalls::sol_get_epoch_stake(core::ptr::null()) }
481 }
482 #[cfg(not(target_os = "solana"))]
483 {
484 0
485 }
486}
487
488// ── LastRestartSlot (SIMD-0047) ─────────────────────────────────────
489
490/// LastRestartSlot sysvar address.
491pub const LAST_RESTART_SLOT_ID: Address =
492 crate::address!("SysvarLastRestartS1ot1111111111111111111111");
493
494/// Read the slot of the last cluster restart (hard fork), or `0` if the
495/// cluster has never been restarted.
496///
497/// This wraps the dedicated `sol_get_last_restart_slot` syscall
498/// (SIMD-0047). Programs that must reason about whether state predates a
499/// restart (oracle freshness, liveness windows) read it here instead of
500/// passing the sysvar as an account.
501#[inline]
502pub fn get_last_restart_slot() -> Result<u64, ProgramError> {
503 #[allow(unused_mut)]
504 let mut slot: u64 = 0;
505 #[cfg(target_os = "solana")]
506 {
507 // SAFETY: the syscall writes a single `u64` into the 8-byte buffer
508 // `slot` points at; `slot` is a live stack local for the call.
509 let rc =
510 unsafe { crate::syscalls::sol_get_last_restart_slot(&mut slot as *mut u64 as *mut u8) };
511 if rc != 0 {
512 return Err(ProgramError::UnsupportedSysvar);
513 }
514 }
515 Ok(slot)
516}
517
518// ── SlotHashes ──────────────────────────────────────────────────────
519
520/// One `(slot, hash)` entry from the SlotHashes sysvar.
521#[derive(Clone, Copy, Debug, PartialEq, Eq)]
522pub struct SlotHash {
523 pub slot: u64,
524 pub hash: [u8; 32],
525}
526
527/// Read the most recent `(slot, hash)` from the SlotHashes sysvar.
528///
529/// SlotHashes is a length-prefixed list ordered most-recent-first:
530/// `u64 count` then `count` entries of `slot(u64) + hash([u8;32])`. This
531/// reads just the count and the first entry (48 bytes total) via
532/// `sol_get_sysvar`, avoiding the cost of materializing the full 16 KiB
533/// sysvar. Returns `Ok(None)` when the list is empty.
534#[inline]
535pub fn slot_hashes_latest() -> Result<Option<SlotHash>, ProgramError> {
536 let mut count_buf = [0u8; 8];
537 get_sysvar_into(&SLOT_HASHES_ID, 0, &mut count_buf)?;
538 let count = u64::from_le_bytes(count_buf);
539 if count == 0 {
540 return Ok(None);
541 }
542 let mut entry = [0u8; 40];
543 get_sysvar_into(&SLOT_HASHES_ID, 8, &mut entry)?;
544 let slot = u64::from_le_bytes([
545 entry[0], entry[1], entry[2], entry[3], entry[4], entry[5], entry[6], entry[7],
546 ]);
547 let mut hash = [0u8; 32];
548 hash.copy_from_slice(&entry[8..40]);
549 Ok(Some(SlotHash { slot, hash }))
550}
551
552// ── StakeHistory ────────────────────────────────────────────────────
553
554/// One epoch's stake-history entry.
555#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
556pub struct StakeHistoryEntry {
557 pub epoch: u64,
558 pub effective: u64,
559 pub activating: u64,
560 pub deactivating: u64,
561}
562
563/// Read the most recent stake-history entry.
564///
565/// StakeHistory is a length-prefixed list ordered most-recent-first:
566/// `u64 count` then entries of `epoch(u64) + effective(u64) +
567/// activating(u64) + deactivating(u64)` (32 bytes each). Returns
568/// `Ok(None)` when the history is empty.
569#[inline]
570pub fn stake_history_latest() -> Result<Option<StakeHistoryEntry>, ProgramError> {
571 let mut count_buf = [0u8; 8];
572 get_sysvar_into(&STAKE_HISTORY_ID, 0, &mut count_buf)?;
573 let count = u64::from_le_bytes(count_buf);
574 if count == 0 {
575 return Ok(None);
576 }
577 let mut entry = [0u8; 32];
578 get_sysvar_into(&STAKE_HISTORY_ID, 8, &mut entry)?;
579 let rd = |o: usize| {
580 u64::from_le_bytes([
581 entry[o],
582 entry[o + 1],
583 entry[o + 2],
584 entry[o + 3],
585 entry[o + 4],
586 entry[o + 5],
587 entry[o + 6],
588 entry[o + 7],
589 ])
590 };
591 Ok(Some(StakeHistoryEntry {
592 epoch: rd(0),
593 effective: rd(8),
594 activating: rd(16),
595 deactivating: rd(24),
596 }))
597}
598
599// ── EpochRewards (SIMD-0118) ────────────────────────────────────────
600
601/// EpochRewards sysvar data (SIMD-0118).
602///
603/// Surfaces the partitioned-rewards distribution state for the current
604/// epoch: how many lamports are being paid out, how far distribution has
605/// progressed, and whether the rewards period is still active. Staking and
606/// airdrop programs read it to gate behaviour during the distribution
607/// window.
608#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
609pub struct EpochRewards {
610 /// First block height at which rewards distribution begins this epoch.
611 pub distribution_starting_block_height: u64,
612 /// Number of partitions the distribution is split across.
613 pub num_partitions: u64,
614 /// Blockhash of the parent of the epoch's first block.
615 pub parent_blockhash: [u8; 32],
616 /// Total rewards points calculated for the epoch.
617 pub total_points: u128,
618 /// Total rewards (lamports) calculated for the epoch.
619 pub total_rewards: u64,
620 /// Rewards (lamports) distributed so far this epoch.
621 pub distributed_rewards: u64,
622 /// Whether the rewards period (calculation + distribution) is active.
623 pub active: bool,
624}
625
626/// Wire size of the bincode-serialized EpochRewards sysvar account data.
627///
628/// `8 + 8 + 32 + 16 + 8 + 8 + 1`. Unlike Clock/Rent/EpochSchedule (which
629/// have dedicated syscalls that memcpy a `#[repr(C)]` struct), EpochRewards
630/// is only reachable through `sol_get_sysvar`, which returns the sysvar
631/// *account data* in bincode form: a flat little-endian field concatenation
632/// with no padding. The decoder below mirrors that image byte-for-byte
633/// rather than casting a `#[repr(C)]` struct (whose `u128` would force
634/// 16-byte alignment and 96-byte size, reading past the 81-byte image).
635const EPOCH_REWARDS_LEN: usize = 81;
636
637#[inline]
638fn decode_epoch_rewards(buf: &[u8; EPOCH_REWARDS_LEN]) -> EpochRewards {
639 let rd8 = |o: usize| {
640 u64::from_le_bytes([
641 buf[o],
642 buf[o + 1],
643 buf[o + 2],
644 buf[o + 3],
645 buf[o + 4],
646 buf[o + 5],
647 buf[o + 6],
648 buf[o + 7],
649 ])
650 };
651 let mut parent_blockhash = [0u8; 32];
652 parent_blockhash.copy_from_slice(&buf[16..48]);
653 let mut points = [0u8; 16];
654 points.copy_from_slice(&buf[48..64]);
655 EpochRewards {
656 distribution_starting_block_height: rd8(0),
657 num_partitions: rd8(8),
658 parent_blockhash,
659 total_points: u128::from_le_bytes(points),
660 total_rewards: rd8(64),
661 distributed_rewards: rd8(72),
662 active: buf[80] != 0,
663 }
664}
665
666/// Read the EpochRewards sysvar (SIMD-0118).
667///
668/// Reads the bincode account-data image via `sol_get_sysvar` and decodes
669/// it without alignment assumptions. Off-chain this returns the zeroed
670/// default (`active = false`). Returns `Err(UnsupportedSysvar)` if the
671/// syscall fails (e.g. the sysvar is unavailable on the cluster).
672#[inline]
673pub fn get_epoch_rewards() -> Result<EpochRewards, ProgramError> {
674 let mut buf = [0u8; EPOCH_REWARDS_LEN];
675 get_sysvar_into(&EPOCH_REWARDS_ID, 0, &mut buf)?;
676 Ok(decode_epoch_rewards(&buf))
677}
678
679#[cfg(test)]
680mod abi_tests {
681 use super::*;
682
683 /// Reproduce the byte image the runtime memcpy's for EpochSchedule and
684 /// confirm every field reads from the offset the syscall writes. This
685 /// is the runtime counterpart to the compile-time offset asserts: it
686 /// proves the read side, not just the struct shape.
687 #[test]
688 fn epoch_schedule_reads_canonical_byte_image() {
689 // Devnet/mainnet default: 432_000 slots/epoch, no warmup.
690 let mut buf = [0u8; 40];
691 buf[0..8].copy_from_slice(&432_000u64.to_le_bytes()); // slots_per_epoch
692 buf[8..16].copy_from_slice(&432_000u64.to_le_bytes()); // leader_schedule_slot_offset
693 buf[16] = 0; // warmup = false
694 // bytes 17..24 are padding
695 buf[24..32].copy_from_slice(&0u64.to_le_bytes()); // first_normal_epoch
696 buf[32..40].copy_from_slice(&0u64.to_le_bytes()); // first_normal_slot
697
698 // SAFETY: `EpochSchedule` is repr(C), size 40, and `buf` is 40 bytes
699 // matching the canonical wire image asserted above.
700 let sched: EpochSchedule = unsafe { core::ptr::read(buf.as_ptr() as *const EpochSchedule) };
701 assert_eq!(sched.slots_per_epoch, 432_000);
702 assert_eq!(sched.leader_schedule_slot_offset, 432_000);
703 assert!(!sched.warmup);
704 assert_eq!(sched.first_normal_epoch, 0);
705 assert_eq!(sched.first_normal_slot, 0);
706 }
707
708 #[test]
709 fn clock_reads_canonical_byte_image() {
710 let mut buf = [0u8; 40];
711 buf[0..8].copy_from_slice(&123u64.to_le_bytes()); // slot
712 buf[8..16].copy_from_slice(&1_600_000_000i64.to_le_bytes()); // epoch_start_timestamp
713 buf[16..24].copy_from_slice(&7u64.to_le_bytes()); // epoch
714 buf[24..32].copy_from_slice(&8u64.to_le_bytes()); // leader_schedule_epoch
715 buf[32..40].copy_from_slice(&1_600_000_500i64.to_le_bytes()); // unix_timestamp
716
717 // SAFETY: `Clock` is repr(C), size 40, matching this 40-byte image.
718 let clock: Clock = unsafe { core::ptr::read(buf.as_ptr() as *const Clock) };
719 assert_eq!(clock.slot, 123);
720 assert_eq!(clock.epoch_start_timestamp, 1_600_000_000);
721 assert_eq!(clock.epoch, 7);
722 assert_eq!(clock.leader_schedule_epoch, 8);
723 assert_eq!(clock.unix_timestamp, 1_600_000_500);
724 }
725
726 /// Build the canonical bincode image for EpochRewards and confirm the
727 /// decoder reads every field from the byte offset the runtime writes.
728 /// This is the read-side proof for the no-dedicated-syscall sysvar.
729 #[test]
730 fn epoch_rewards_decodes_canonical_byte_image() {
731 let mut buf = [0u8; EPOCH_REWARDS_LEN];
732 buf[0..8].copy_from_slice(&100u64.to_le_bytes()); // distribution_starting_block_height
733 buf[8..16].copy_from_slice(&8u64.to_le_bytes()); // num_partitions
734 buf[16..48].copy_from_slice(&[7u8; 32]); // parent_blockhash
735 buf[48..64].copy_from_slice(&123_456_789u128.to_le_bytes()); // total_points
736 buf[64..72].copy_from_slice(&5_000_000u64.to_le_bytes()); // total_rewards
737 buf[72..80].copy_from_slice(&1_250_000u64.to_le_bytes()); // distributed_rewards
738 buf[80] = 1; // active = true
739
740 let er = decode_epoch_rewards(&buf);
741 assert_eq!(er.distribution_starting_block_height, 100);
742 assert_eq!(er.num_partitions, 8);
743 assert_eq!(er.parent_blockhash, [7u8; 32]);
744 assert_eq!(er.total_points, 123_456_789);
745 assert_eq!(er.total_rewards, 5_000_000);
746 assert_eq!(er.distributed_rewards, 1_250_000);
747 assert!(er.active);
748 }
749
750 #[test]
751 fn epoch_rewards_off_chain_is_zeroed_default() {
752 // Off-chain `get_sysvar_into` is a no-op, so the getter yields the
753 // zeroed default with `active = false`.
754 let er = get_epoch_rewards().unwrap();
755 assert_eq!(er, EpochRewards::default());
756 assert!(!er.active);
757 }
758}
759
760#[cfg(test)]
761mod rent_tests {
762 use super::*;
763
764 /// Loader bound on serialized account data (10 MiB), the largest
765 /// `data_len` any rent calculation ever sees on-chain.
766 const LOADER_MAX_DATA_LEN: usize = 10_485_760;
767
768 /// Transcription of Solana's `Rent::minimum_balance_unchecked`, including
769 /// the SIMD-0194 `1.0` and legacy `2.0` integer fast paths.
770 fn solana_reference_minimum_balance(data_len: usize, lpby: u64, threshold: f64) -> u64 {
771 let bytes = data_len as u64;
772 let integer_part = (ACCOUNT_STORAGE_OVERHEAD + bytes).saturating_mul(lpby);
773 if threshold == 1.0 {
774 integer_part
775 } else if threshold == 2.0 {
776 integer_part.saturating_mul(2)
777 } else {
778 (integer_part as f64 * threshold) as u64
779 }
780 }
781
782 fn rent_with(lpby: u64, threshold: f64) -> Rent {
783 Rent {
784 lamports_per_byte_year: lpby,
785 exemption_threshold: threshold,
786 burn_percent: 0,
787 }
788 }
789
790 /// The const fast-path must not overflow or panic at the data-length
791 /// extremes the loader permits (0 and 10 MiB).
792 #[test]
793 fn const_rent_exempt_minimum_no_overflow_at_extremes() {
794 assert_eq!(
795 rent_exempt_minimum(0),
796 ACCOUNT_STORAGE_OVERHEAD * LAMPORTS_PER_BYTE_YEAR * EXEMPTION_THRESHOLD_YEARS
797 );
798 let max = rent_exempt_minimum(LOADER_MAX_DATA_LEN);
799 let expected = (LOADER_MAX_DATA_LEN as u64 + ACCOUNT_STORAGE_OVERHEAD)
800 * LAMPORTS_PER_BYTE_YEAR
801 * EXEMPTION_THRESHOLD_YEARS;
802 assert_eq!(max, expected);
803 // Sanity: comfortably inside u64.
804 assert!(max < u64::MAX / 2);
805 }
806
807 /// The sysvar path must not overflow or panic even with a wildly
808 /// repriced `lamports_per_byte_year` at the maximum data length: the
809 /// saturating integer product keeps the arithmetic safe.
810 #[test]
811 fn sysvar_minimum_balance_no_overflow_at_extremes() {
812 // Far past any realistic rate.
813 let rent = rent_with(1u64 << 40, 2.0);
814 let _ = rent.minimum_balance(LOADER_MAX_DATA_LEN);
815 // data_len = 0 lower extreme.
816 assert_eq!(
817 rent.minimum_balance(0),
818 solana_reference_minimum_balance(0, 1u64 << 40, 2.0)
819 );
820 }
821
822 /// The const path and sysvar path agree at the launch-era snapshot.
823 #[test]
824 fn const_and_sysvar_agree_at_launch_snapshot() {
825 let rent = rent_with(LAMPORTS_PER_BYTE_YEAR, EXEMPTION_THRESHOLD_YEARS as f64);
826 for &dl in &[
827 0usize,
828 1,
829 127,
830 128,
831 1024,
832 10_240,
833 1_000_000,
834 LOADER_MAX_DATA_LEN,
835 ] {
836 assert_eq!(
837 rent_exempt_minimum(dl),
838 rent.minimum_balance(dl),
839 "const vs sysvar mismatch at data_len={dl}"
840 );
841 }
842 }
843
844 /// The sysvar path must byte-match Solana's runtime formula across a
845 /// range of data sizes, repriced per-byte costs, and fractional
846 /// thresholds, including a `lamports_per_byte_year` past f64's 53-bit
847 /// exact-integer range, where the old all-f64 form could drift.
848 #[test]
849 fn sysvar_minimum_balance_byte_matches_solana_reference() {
850 let cases: &[(usize, u64, f64)] = &[
851 (0, 6_333, 1.0),
852 (167_829, 6_333, 1.0),
853 (0, 3_480, 2.0),
854 (165, 3_480, 2.0),
855 (10_240, 3_480, 2.0),
856 (1_000_000, 6_960, 2.0), // hypothetical 2x reprice
857 (500_000, 3_480, 3.0), // whole-year non-default threshold
858 (10_485_760, 3_480, 2.0), // max size
859 // `lpby` just past f64's 2^53 exact-integer range, the case
860 // that motivates the integer product + single f64 step (an
861 // all-f64 formula would lose a lamport here). `data_len` is
862 // kept small so the *integer* product stays inside u64: at
863 // this `lpby`, (overhead + data_len) must be < ~2044 or the
864 // product overflows u64 entirely (a regime Solana itself
865 // never reaches, `lpby` is a fixed cluster constant).
866 (1_024, 9_007_199_254_740_993, 2.0),
867 ];
868 for &(dl, lpby, threshold) in cases {
869 let rent = rent_with(lpby, threshold);
870 assert_eq!(
871 rent.minimum_balance(dl),
872 solana_reference_minimum_balance(dl, lpby, threshold),
873 "sysvar minimum_balance != Solana reference at dl={dl}, lpby={lpby}, threshold={threshold}"
874 );
875 }
876 }
877
878 /// The float-free threshold scaling: exact for the two thresholds any
879 /// cluster has stored, and for every other finite positive threshold a
880 /// whole-year round-up that is never below the float cast the runtime
881 /// historically used.
882 #[test]
883 fn saturating_mul_u64_matches_the_library_operator() {
884 let samples = [
885 0u64,
886 1,
887 2,
888 3,
889 128,
890 153,
891 3_480,
892 5_080,
893 6_333,
894 10_485_888,
895 u32::MAX as u64,
896 u32::MAX as u64 + 1,
897 1 << 40,
898 u64::MAX / 3,
899 u64::MAX / 2,
900 u64::MAX / 2 + 1,
901 u64::MAX - 1,
902 u64::MAX,
903 ];
904 for &a in &samples {
905 for &b in &samples {
906 assert_eq!(saturating_mul_u64(a, b), a.saturating_mul(b), "{a} * {b}");
907 }
908 }
909 }
910
911 #[test]
912 fn threshold_scaling_is_float_free_and_never_underfunds() {
913 fn reference(integer_part: u64, threshold: f64) -> u64 {
914 (integer_part as f64 * threshold) as u64
915 }
916 let amounts: [u64; 8] = [
917 0,
918 1,
919 7,
920 128 * 3_480,
921 890_880,
922 1_740_445_440,
923 1 << 40,
924 1 << 52,
925 ];
926 let whole_thresholds: [f64; 4] = [1.0, 2.0, 3.0, 4.0];
927 let fractional: [(f64, u64); 10] = [
928 (0.5, 1),
929 (1.5, 2),
930 (2.5, 3),
931 (0.25, 1),
932 (0.75, 1),
933 (1.1, 2),
934 (1.7, 2),
935 (0.3, 1),
936 (2.9, 3),
937 (3.33, 4),
938 ];
939 for &amount in &amounts {
940 for &threshold in &whole_thresholds {
941 assert_eq!(
942 scale_by_exemption_threshold(amount, threshold.to_bits()),
943 reference(amount, threshold),
944 "amount {amount} threshold {threshold}"
945 );
946 }
947 for &(threshold, years) in &fractional {
948 let ours = scale_by_exemption_threshold(amount, threshold.to_bits());
949 assert_eq!(ours, amount.saturating_mul(years), "threshold {threshold}");
950 assert!(
951 ours >= reference(amount, threshold),
952 "amount {amount} threshold {threshold}: {ours} underfunds"
953 );
954 }
955 // Degenerate thresholds: zero, negative, and NaN yield 0 like the
956 // cast; a subnormal rounds up to one year; infinity saturates.
957 assert_eq!(scale_by_exemption_threshold(amount, 0.0f64.to_bits()), 0);
958 assert_eq!(scale_by_exemption_threshold(amount, (-1.0f64).to_bits()), 0);
959 assert_eq!(scale_by_exemption_threshold(amount, f64::NAN.to_bits()), 0);
960 assert_eq!(
961 scale_by_exemption_threshold(amount, f64::MIN_POSITIVE.to_bits() >> 1),
962 amount
963 );
964 let saturated = if amount == 0 { 0 } else { u64::MAX };
965 assert_eq!(
966 scale_by_exemption_threshold(amount, f64::INFINITY.to_bits()),
967 saturated
968 );
969 }
970 // Huge thresholds saturate.
971 let huge: f64 = (1u64 << 63) as f64;
972 assert_eq!(scale_by_exemption_threshold(2, huge.to_bits()), u64::MAX);
973 let astronomical: f64 = 1e300;
974 assert_eq!(
975 scale_by_exemption_threshold(1, astronomical.to_bits()),
976 u64::MAX
977 );
978 let exactly_2_pow_60: f64 = (1u64 << 60) as f64;
979 assert_eq!(
980 scale_by_exemption_threshold(1, exactly_2_pow_60.to_bits()),
981 1 << 60
982 );
983 assert_eq!(
984 scale_by_exemption_threshold(u64::MAX, 1.0f64.to_bits()),
985 u64::MAX
986 );
987 assert_eq!(
988 scale_by_exemption_threshold(u64::MAX, 2.0f64.to_bits()),
989 u64::MAX
990 );
991 }
992
993 /// The correctness gap the safety work closes: after an UPWARD rent
994 /// reprice the live sysvar demands strictly more than the const path,
995 /// so gating reaping on the const would under-fund.
996 #[test]
997 fn repriced_sysvar_exceeds_const_underestimate() {
998 let dl = 4_096;
999 let const_min = rent_exempt_minimum(dl);
1000 // Cluster doubled the per-byte cost.
1001 let repriced = rent_with(LAMPORTS_PER_BYTE_YEAR * 2, EXEMPTION_THRESHOLD_YEARS as f64);
1002 let sysvar_min = repriced.minimum_balance(dl);
1003 assert!(
1004 sysvar_min > const_min,
1005 "expected repriced sysvar minimum ({sysvar_min}) > const minimum ({const_min})"
1006 );
1007 assert_eq!(sysvar_min, const_min * 2);
1008 }
1009}
1010
1011// =====================================================================
1012// Kani proof harnesses for the rent arithmetic.
1013// =====================================================================
1014//
1015// These mirror the existing native Kani conventions (see
1016// `raw_input.rs::kani_proofs`): a `#[cfg(kani)]` module of `#[kani::proof]`
1017// harnesses over symbolic inputs bounded by the loader's limits, run by
1018// `cargo kani -p hopper-native`. They discharge the three safety claims the
1019// SAFETY-RENT work rests on:
1020//
1021// (1) the const fast path cannot overflow for any loader-permitted
1022// `data_len`;
1023// (2) the sysvar path's integer product cannot overflow for any
1024// loader-permitted `data_len` and any realistic (repriced)
1025// `lamports_per_byte_year`, and its saturating ops never actually
1026// saturate in that range (so the byte-match with the runtime is
1027// exact); and
1028// (3) the const and sysvar forms AGREE at the launch-era snapshot.
1029#[cfg(kani)]
1030mod kani_rent_proofs {
1031 use super::*;
1032
1033 /// Loader bound on serialized account data (10 MiB).
1034 const LOADER_MAX_DATA_LEN: usize = 10_485_760;
1035
1036 /// Generous upper bound on a repriced `lamports_per_byte_year`: 2^40 is
1037 /// ~1.1e12 and far past any realistic rent change,
1038 /// yet the integer product still provably cannot overflow u64.
1039 const MAX_LAMPORTS_PER_BYTE_YEAR: u64 = 1 << 40;
1040
1041 /// The const path's integer arithmetic never overflows for any
1042 /// loader-permitted `data_len`, and the function returns exactly the
1043 /// checked value.
1044 #[kani::proof]
1045 fn const_rent_exempt_minimum_never_overflows() {
1046 let data_len: usize = kani::any();
1047 kani::assume(data_len <= LOADER_MAX_DATA_LEN);
1048
1049 // Each `checked_*` doubles as the no-overflow proof for the `*`/`+`
1050 // in `rent_exempt_minimum`.
1051 let sum = (data_len as u64)
1052 .checked_add(ACCOUNT_STORAGE_OVERHEAD)
1053 .unwrap();
1054 let per_year = sum.checked_mul(LAMPORTS_PER_BYTE_YEAR).unwrap();
1055 let total = per_year.checked_mul(EXEMPTION_THRESHOLD_YEARS).unwrap();
1056
1057 assert_eq!(rent_exempt_minimum(data_len), total);
1058 }
1059
1060 /// The sysvar path's integer product never overflows for any
1061 /// loader-permitted `data_len` and any realistic `lamports_per_byte_year`,
1062 /// and its saturating ops equal the checked ops (never saturate) in that
1063 /// range.
1064 #[kani::proof]
1065 fn sysvar_minimum_balance_integer_part_never_overflows() {
1066 let data_len: usize = kani::any();
1067 let lpby: u64 = kani::any();
1068 kani::assume(data_len <= LOADER_MAX_DATA_LEN);
1069 kani::assume(lpby <= MAX_LAMPORTS_PER_BYTE_YEAR);
1070
1071 let bytes = data_len as u64;
1072 let checked = ACCOUNT_STORAGE_OVERHEAD
1073 .checked_add(bytes)
1074 .and_then(|s| s.checked_mul(lpby));
1075 assert!(checked.is_some());
1076
1077 let saturating = ACCOUNT_STORAGE_OVERHEAD
1078 .saturating_add(bytes)
1079 .saturating_mul(lpby);
1080 assert_eq!(saturating, checked.unwrap());
1081 }
1082
1083 /// Const and sysvar forms agree at the launch-era snapshot.
1084 #[kani::proof]
1085 fn const_and_sysvar_agree_at_launch_snapshot() {
1086 let data_len: usize = kani::any();
1087 kani::assume(data_len <= LOADER_MAX_DATA_LEN);
1088
1089 let rent = Rent {
1090 lamports_per_byte_year: LAMPORTS_PER_BYTE_YEAR,
1091 exemption_threshold: EXEMPTION_THRESHOLD_YEARS as f64,
1092 burn_percent: 0,
1093 };
1094
1095 assert_eq!(
1096 rent_exempt_minimum(data_len),
1097 rent.minimum_balance(data_len)
1098 );
1099 }
1100}