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