Skip to main content

hopper_core/sysvar/
cache.rs

1//! Sysvar context cache -- avoid repeated syscall reads.
2//!
3//! Inspired by Star Frame's context caching pattern. When multiple
4//! checks in one instruction need the same sysvar (e.g., clock for
5//! deadline + staleness), reading it once and caching saves ~100+ CU
6//! per duplicate read.
7//!
8//! All caching is stack-local -- no global state, no heap.
9
10use hopper_runtime::error::ProgramError;
11
12/// Cached Clock sysvar fields.
13///
14/// Created once per instruction, used by multiple checks.
15/// Each field is `Option` -- populated lazily on first access
16/// from account data.
17pub struct CachedClock {
18    pub slot: u64,
19    pub epoch: u64,
20    pub unix_timestamp: i64,
21}
22
23impl CachedClock {
24    /// Parse Clock sysvar from account data (40 bytes).
25    ///
26    /// Call once at the start of your instruction, then pass
27    /// the cached value to all checks that need clock data.
28    #[inline]
29    pub fn from_account_data(data: &[u8]) -> Result<Self, ProgramError> {
30        if data.len() < 40 {
31            return Err(ProgramError::InvalidAccountData);
32        }
33        Ok(Self {
34            slot: u64::from_le_bytes([
35                data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7],
36            ]),
37            epoch: u64::from_le_bytes([
38                data[16], data[17], data[18], data[19], data[20], data[21], data[22], data[23],
39            ]),
40            unix_timestamp: i64::from_le_bytes([
41                data[32], data[33], data[34], data[35], data[36], data[37], data[38], data[39],
42            ]),
43        })
44    }
45
46    /// Check that a deadline has not passed.
47    #[inline(always)]
48    pub fn check_not_expired(&self, deadline: i64) -> Result<(), ProgramError> {
49        if self.unix_timestamp > deadline {
50            return Err(ProgramError::InvalidAccountData);
51        }
52        Ok(())
53    }
54
55    /// Check that a deadline HAS passed (for claiming, unlocking, etc.).
56    #[inline(always)]
57    pub fn check_expired(&self, deadline: i64) -> Result<(), ProgramError> {
58        if self.unix_timestamp <= deadline {
59            return Err(ProgramError::InvalidAccountData);
60        }
61        Ok(())
62    }
63
64    /// Check that now is within a time window [start, end].
65    #[inline(always)]
66    pub fn check_within_window(&self, start: i64, end: i64) -> Result<(), ProgramError> {
67        if self.unix_timestamp < start || self.unix_timestamp > end {
68            return Err(ProgramError::InvalidAccountData);
69        }
70        Ok(())
71    }
72
73    /// Check cooldown: enough time has passed since last action.
74    #[inline(always)]
75    pub fn check_cooldown(&self, last_action: i64, cooldown_secs: i64) -> Result<(), ProgramError> {
76        if self.unix_timestamp < last_action + cooldown_secs {
77            return Err(ProgramError::InvalidAccountData);
78        }
79        Ok(())
80    }
81
82    /// Check slot staleness: last_update_slot is within max_age of current slot.
83    #[inline(always)]
84    pub fn check_slot_staleness(
85        &self,
86        last_update_slot: u64,
87        max_age: u64,
88    ) -> Result<(), ProgramError> {
89        if self.slot.saturating_sub(last_update_slot) > max_age {
90            return Err(ProgramError::InvalidAccountData);
91        }
92        Ok(())
93    }
94}
95
96/// Cached Rent sysvar fields.
97pub struct CachedRent {
98    pub lamports_per_byte_year: u64,
99}
100
101impl CachedRent {
102    /// Parse Rent sysvar from account data.
103    #[inline]
104    pub fn from_account_data(data: &[u8]) -> Result<Self, ProgramError> {
105        if data.len() < 8 {
106            return Err(ProgramError::InvalidAccountData);
107        }
108        Ok(Self {
109            lamports_per_byte_year: u64::from_le_bytes([
110                data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7],
111            ]),
112        })
113    }
114
115    /// Compute rent-exempt minimum for a given data size.
116    #[inline(always)]
117    pub fn exempt_min(&self, data_len: usize) -> u64 {
118        // Standard formula: (128 + data_len) * lamports_per_byte_year * 2 / 365.25 / 86400
119        // Simplified to the common approximation used by Solana:
120        ((128 + data_len) as u64) * 6960
121    }
122}
123
124/// Combined sysvar context for a single instruction.
125///
126/// Parse all needed sysvars once at the top of your instruction handler,
127/// then pass this context to all validation functions.
128///
129/// ```ignore
130/// let ctx = SysvarContext::new()
131///     .with_clock(&clock_account)?
132///     .with_rent(&rent_account)?;
133///
134/// ctx.clock()?.check_not_expired(deadline)?;
135/// ctx.clock()?.check_slot_staleness(oracle_slot, 50)?;
136/// ```
137pub struct SysvarContext {
138    clock: Option<CachedClock>,
139    rent: Option<CachedRent>,
140}
141
142impl SysvarContext {
143    /// Create an empty context.
144    #[inline(always)]
145    pub const fn new() -> Self {
146        Self {
147            clock: None,
148            rent: None,
149        }
150    }
151
152    /// Parse and cache the Clock sysvar.
153    #[inline]
154    pub fn with_clock(mut self, clock_data: &[u8]) -> Result<Self, ProgramError> {
155        self.clock = Some(CachedClock::from_account_data(clock_data)?);
156        Ok(self)
157    }
158
159    /// Parse and cache the Rent sysvar.
160    #[inline]
161    pub fn with_rent(mut self, rent_data: &[u8]) -> Result<Self, ProgramError> {
162        self.rent = Some(CachedRent::from_account_data(rent_data)?);
163        Ok(self)
164    }
165
166    /// Get the cached Clock. Returns error if not initialized.
167    #[inline(always)]
168    pub fn clock(&self) -> Result<&CachedClock, ProgramError> {
169        match &self.clock {
170            Some(c) => Ok(c),
171            None => Err(ProgramError::InvalidArgument),
172        }
173    }
174
175    /// Get the cached Rent. Returns error if not initialized.
176    #[inline(always)]
177    pub fn rent(&self) -> Result<&CachedRent, ProgramError> {
178        match &self.rent {
179            Some(r) => Ok(r),
180            None => Err(ProgramError::InvalidArgument),
181        }
182    }
183
184    /// Check if clock is available.
185    #[inline(always)]
186    pub fn has_clock(&self) -> bool {
187        self.clock.is_some()
188    }
189
190    /// Check if rent is available.
191    #[inline(always)]
192    pub fn has_rent(&self) -> bool {
193        self.rent.is_some()
194    }
195}
196
197impl Default for SysvarContext {
198    fn default() -> Self {
199        Self::new()
200    }
201}