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. No framework
5//! wraps the epoch schedule sysvar at the native level.
6
7use crate::address::Address;
8use crate::error::ProgramError;
9
10// ── Clock ────────────────────────────────────────────────────────────
11
12/// Clock sysvar data, read directly from the runtime.
13#[repr(C)]
14#[derive(Clone, Copy, Debug, Default)]
15pub struct Clock {
16    pub slot: u64,
17    pub epoch_start_timestamp: i64,
18    pub epoch: u64,
19    pub leader_schedule_epoch: u64,
20    pub unix_timestamp: i64,
21}
22
23/// Read the Clock sysvar.
24#[inline]
25pub fn get_clock() -> Result<Clock, ProgramError> {
26    #[allow(unused_mut)]
27    let mut clock = Clock::default();
28
29    #[cfg(target_os = "solana")]
30    {
31        let rc =
32            // SAFETY: This block is part of Hopper's audited zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
33            unsafe { crate::syscalls::sol_get_clock_sysvar(&mut clock as *mut Clock as *mut u8) };
34        if rc != 0 {
35            return Err(ProgramError::UnsupportedSysvar);
36        }
37    }
38
39    Ok(clock)
40}
41
42// ── Rent ─────────────────────────────────────────────────────────────
43
44/// Rent sysvar data.
45#[repr(C)]
46#[derive(Clone, Copy, Debug, Default)]
47pub struct Rent {
48    pub lamports_per_byte_year: u64,
49    pub exemption_threshold: f64,
50    pub burn_percent: u8,
51}
52
53/// Lamports charged per byte of account storage per year.
54pub const LAMPORTS_PER_BYTE_YEAR: u64 = 3_480;
55
56/// Years of rent an account must prepay to be rent-exempt.
57pub const EXEMPTION_THRESHOLD_YEARS: u64 = 2;
58
59/// Fixed per-account storage overhead charged by the cluster.
60pub const ACCOUNT_STORAGE_OVERHEAD: u64 = 128;
61
62/// Minimum balance for rent exemption under the current Solana cluster constants.
63#[inline]
64pub const fn rent_exempt_minimum(data_len: usize) -> u64 {
65    (data_len as u64 + ACCOUNT_STORAGE_OVERHEAD)
66        * LAMPORTS_PER_BYTE_YEAR
67        * EXEMPTION_THRESHOLD_YEARS
68}
69
70/// Read the Rent sysvar.
71#[inline]
72pub fn get_rent() -> Result<Rent, ProgramError> {
73    #[allow(unused_mut)]
74    let mut rent = Rent::default();
75
76    #[cfg(target_os = "solana")]
77    {
78        // SAFETY: This block is part of Hopper's audited zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
79        let rc = unsafe { crate::syscalls::sol_get_rent_sysvar(&mut rent as *mut Rent as *mut u8) };
80        if rc != 0 {
81            return Err(ProgramError::UnsupportedSysvar);
82        }
83    }
84
85    Ok(rent)
86}
87
88impl Rent {
89    /// Calculate the minimum lamports for rent exemption at the given data size.
90    #[inline]
91    pub fn minimum_balance(&self, data_len: usize) -> u64 {
92        let total_size = (data_len as u64).saturating_add(ACCOUNT_STORAGE_OVERHEAD);
93        (total_size as f64 * self.lamports_per_byte_year as f64 * self.exemption_threshold) as u64
94    }
95}
96
97// ── Epoch Schedule ───────────────────────────────────────────────────
98
99/// Epoch schedule sysvar data.
100///
101/// Nobody wraps this at the native level. Useful for programs that
102/// need to reason about epoch boundaries (staking, vesting, time locks).
103#[repr(C)]
104#[derive(Clone, Copy, Debug, Default)]
105pub struct EpochSchedule {
106    pub slots_per_epoch: u64,
107    pub leader_schedule_slot_offset: u64,
108    pub warmup: bool,
109    pub first_normal_epoch: u64,
110    pub first_normal_slot: u64,
111}
112
113/// Read the EpochSchedule sysvar.
114#[inline]
115pub fn get_epoch_schedule() -> Result<EpochSchedule, ProgramError> {
116    #[allow(unused_mut)]
117    let mut schedule = EpochSchedule::default();
118
119    #[cfg(target_os = "solana")]
120    {
121        // SAFETY: This block is part of Hopper's audited zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
122        let rc = unsafe {
123            crate::syscalls::sol_get_epoch_schedule_sysvar(
124                &mut schedule as *mut EpochSchedule as *mut u8,
125            )
126        };
127        if rc != 0 {
128            return Err(ProgramError::UnsupportedSysvar);
129        }
130    }
131
132    Ok(schedule)
133}
134
135impl EpochSchedule {
136    /// Get the epoch for a given slot.
137    #[inline]
138    pub fn get_epoch(&self, slot: u64) -> u64 {
139        if slot < self.first_normal_slot {
140            // During warmup, epoch length doubles each epoch.
141            // Initial epoch has 32 slots (MINIMUM_SLOTS_PER_EPOCH).
142            if slot == 0 {
143                return 0;
144            }
145            // log2(slot / 32) + 1, clamped.
146            let mut epoch_len: u64 = 32; // MINIMUM_SLOTS_PER_EPOCH
147            let mut epoch: u64 = 0;
148            let mut slot_remaining = slot;
149            while slot_remaining >= epoch_len {
150                slot_remaining -= epoch_len;
151                epoch += 1;
152                epoch_len = epoch_len.saturating_mul(2);
153            }
154            epoch
155        } else {
156            let normal_slot_index = slot - self.first_normal_slot;
157            self.first_normal_epoch + normal_slot_index / self.slots_per_epoch
158        }
159    }
160
161    /// Get the first slot in the given epoch.
162    #[inline]
163    pub fn get_first_slot_in_epoch(&self, epoch: u64) -> u64 {
164        if epoch <= self.first_normal_epoch {
165            // Warmup: each epoch doubles in length starting from 32.
166            if epoch == 0 {
167                return 0;
168            }
169            // First slot = sum of all previous epoch lengths.
170            // = 32 * (2^epoch - 1)
171            let shift = epoch.min(63);
172            32_u64.saturating_mul((1_u64 << shift).saturating_sub(1))
173        } else {
174            let normal_epoch_index = epoch - self.first_normal_epoch;
175            self.first_normal_slot + normal_epoch_index * self.slots_per_epoch
176        }
177    }
178}
179
180// ── Well-known sysvar addresses ──────────────────────────────────────
181
182/// Clock sysvar address.
183pub const CLOCK_ID: Address = crate::address!("SysvarC1ock11111111111111111111111111111111");
184
185/// Rent sysvar address.
186pub const RENT_ID: Address = crate::address!("SysvarRent111111111111111111111111111111111");
187
188/// Epoch schedule sysvar address.
189pub const EPOCH_SCHEDULE_ID: Address =
190    crate::address!("SysvarEpochSchedu1e111111111111111111111111");