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}