Skip to main content

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}