light_compressible/
compression_info.rs

1use aligned_sized::aligned_sized;
2use bytemuck::{Pod, Zeroable};
3use light_program_profiler::profile;
4use light_zero_copy::{ZeroCopy, ZeroCopyMut};
5use zerocopy::U64;
6
7use crate::{
8    config::CompressibleConfig,
9    error::CompressibleError,
10    rent::{get_last_funded_epoch, AccountRentState, RentConfig, SLOTS_PER_EPOCH},
11    AnchorDeserialize, AnchorSerialize,
12};
13
14/// Compressible extension for ctoken accounts.
15#[derive(
16    Debug,
17    Clone,
18    Hash,
19    Copy,
20    PartialEq,
21    Eq,
22    Default,
23    AnchorSerialize,
24    AnchorDeserialize,
25    ZeroCopy,
26    ZeroCopyMut,
27    Pod,
28    Zeroable,
29)]
30#[repr(C)]
31#[aligned_sized]
32pub struct CompressionInfo {
33    pub config_account_version: u16, // config_account_version 0 is uninitialized, default is 1
34    /// Compress to account pubkey instead of account owner.
35    pub compress_to_pubkey: u8,
36    /// Version of the compressed token account when ctoken account is
37    /// compressed and closed. (The account_version specifies the hashing scheme.)
38    pub account_version: u8,
39    /// Lamports amount the account is topped up with at every write
40    /// by the fee payer.
41    pub lamports_per_write: u32,
42    /// Authority that can compress and close the account.
43    pub compression_authority: [u8; 32],
44    /// Recipient for rent exemption lamports up on account closure.
45    pub rent_sponsor: [u8; 32],
46    /// Last slot rent was claimed from this account.
47    pub last_claimed_slot: u64,
48    /// Rent exemption lamports paid at account creation.
49    /// Used instead of querying the Rent sysvar to ensure rent sponsor
50    /// gets back exactly what they paid regardless of future rent changes.
51    pub rent_exemption_paid: u32,
52    /// Reserved for future use.
53    pub _reserved: u32,
54    /// Rent function parameters,
55    /// used to calculate whether the account is compressible.
56    pub rent_config: RentConfig,
57}
58
59// Unified macro for all compressible extension types
60macro_rules! impl_is_compressible {
61    ($struct_name:ty) => {
62        impl $struct_name {
63            /// current - last epoch = num epochs due
64            /// rent_due
65            /// available_balance = current_lamports - last_lamports
66            ///     (we can never claim more lamports than rent is due)
67            /// remaining_balance = available_balance - rent_due
68            #[profile]
69            pub fn is_compressible(
70                &self,
71                bytes: u64,
72                current_slot: u64,
73                current_lamports: u64,
74            ) -> Result<Option<u64>, CompressibleError> {
75                let rent_exemption_paid: u32 = self.rent_exemption_paid.into();
76                let rent_exemption_lamports: u64 = rent_exemption_paid as u64;
77                Ok(crate::rent::AccountRentState {
78                    num_bytes: bytes,
79                    current_slot,
80                    current_lamports,
81                    last_claimed_slot: self.last_claimed_slot.into(),
82                }
83                .is_compressible(&self.rent_config, rent_exemption_lamports))
84            }
85
86            /// Converts the `compress_to_pubkey` field into a boolean.
87            pub fn compress_to_pubkey(&self) -> bool {
88                self.compress_to_pubkey != 0
89            }
90
91            /// Calculate the amount of lamports to top up during a write operation.
92            /// Returns 0 if no top-up is needed (account is well-funded).
93            /// Returns write_top_up + rent_deficit if account is compressible.
94            /// Returns write_top_up if account needs more funding but isn't compressible yet.
95            #[profile]
96            pub fn calculate_top_up_lamports(
97                &self,
98                num_bytes: u64,
99                current_slot: u64,
100                current_lamports: u64,
101            ) -> Result<u64, CompressibleError> {
102                let lamports_per_write: u32 = self.lamports_per_write.into();
103                let rent_exemption_paid: u32 = self.rent_exemption_paid.into();
104                let rent_exemption_lamports: u64 = rent_exemption_paid as u64;
105
106                // Calculate rent status using AccountRentState
107                let state = crate::rent::AccountRentState {
108                    num_bytes,
109                    current_slot,
110                    current_lamports,
111                    last_claimed_slot: self.last_claimed_slot.into(),
112                };
113                let is_compressible =
114                    state.is_compressible(&self.rent_config, rent_exemption_lamports);
115                if let Some(rent_deficit) = is_compressible {
116                    Ok(lamports_per_write as u64 + rent_deficit)
117                } else {
118                    let epochs_funded_ahead =
119                        state.epochs_funded_ahead(&self.rent_config, rent_exemption_lamports);
120
121                    // Skip top-up if already funded for max_funded_epochs or more
122                    if epochs_funded_ahead >= self.rent_config.max_funded_epochs as u64 {
123                        Ok(0)
124                    } else {
125                        Ok(lamports_per_write as u64)
126                    }
127                }
128            }
129        }
130    };
131}
132impl_is_compressible!(CompressionInfo);
133impl_is_compressible!(ZCompressionInfo<'_>);
134impl_is_compressible!(ZCompressionInfoMut<'_>);
135
136// Unified macro to implement get_last_funded_epoch for all extension types
137macro_rules! impl_get_last_paid_epoch {
138    ($struct_name:ty) => {
139        impl $struct_name {
140            /// Get the last epoch that has been paid for.
141            /// Returns the epoch number through which rent has been prepaid.
142            pub fn get_last_funded_epoch(
143                &self,
144                num_bytes: u64,
145                current_lamports: u64,
146                rent_exemption_lamports: u64,
147            ) -> Result<u64, CompressibleError> {
148                Ok(get_last_funded_epoch(
149                    num_bytes,
150                    current_lamports,
151                    self.last_claimed_slot,
152                    &self.rent_config,
153                    rent_exemption_lamports,
154                ))
155            }
156        }
157    };
158}
159
160impl_get_last_paid_epoch!(CompressionInfo);
161impl_get_last_paid_epoch!(ZCompressionInfo<'_>);
162impl_get_last_paid_epoch!(ZCompressionInfoMut<'_>);
163
164pub struct ClaimAndUpdate<'a> {
165    pub compression_authority: &'a [u8; 32],
166    pub rent_sponsor: &'a [u8; 32],
167    pub config_account: &'a CompressibleConfig,
168    pub bytes: u64,
169    pub current_slot: u64,
170    pub current_lamports: u64,
171}
172
173impl ZCompressionInfoMut<'_> {
174    /// Claim rent for past completed epochs and update the extension state.
175    /// Returns the amount of lamports claimed, or None if account should be compressed.
176    pub fn claim(
177        &mut self,
178        num_bytes: u64,
179        current_slot: u64,
180        current_lamports: u64,
181    ) -> Result<Option<u64>, CompressibleError> {
182        let rent_exemption_paid: u32 = self.rent_exemption_paid.into();
183        let rent_exemption_lamports: u64 = rent_exemption_paid as u64;
184        let state = AccountRentState {
185            num_bytes,
186            current_slot,
187            current_lamports,
188            last_claimed_slot: self.last_claimed_slot.into(),
189        };
190        let claimed = state.calculate_claimable_rent(&self.rent_config, rent_exemption_lamports);
191
192        if let Some(claimed_amount) = claimed {
193            if claimed_amount > 0 {
194                let completed_epochs = state.get_completed_epochs();
195
196                self.last_claimed_slot += U64::from(completed_epochs * SLOTS_PER_EPOCH);
197                Ok(Some(claimed_amount))
198            } else {
199                Ok(None)
200            }
201        } else {
202            Ok(None)
203        }
204    }
205
206    pub fn claim_and_update(
207        &mut self,
208        ClaimAndUpdate {
209            compression_authority,
210            rent_sponsor,
211            config_account,
212            bytes,
213            current_slot,
214            current_lamports,
215        }: ClaimAndUpdate,
216    ) -> Result<Option<u64>, CompressibleError> {
217        if self.compression_authority != *compression_authority {
218            #[cfg(feature = "solana")]
219            solana_msg::msg!("Rent authority mismatch");
220            return Ok(None);
221        }
222        if self.rent_sponsor != *rent_sponsor {
223            #[cfg(feature = "solana")]
224            solana_msg::msg!("Rent sponsor PDA does not match rent recipient");
225            return Ok(None);
226        }
227
228        // Verify config version matches
229        let account_version: u16 = self.config_account_version.into();
230        let config_version = config_account.version;
231
232        if account_version != config_version {
233            #[cfg(feature = "solana")]
234            solana_msg::msg!(
235                "Config version mismatch: account has v{}, config is v{}",
236                account_version,
237                config_version
238            );
239            return Err(CompressibleError::InvalidVersion);
240        }
241
242        let claim_result = self.claim(bytes, current_slot, current_lamports)?;
243
244        // Update RentConfig after claim calculation (even if claim_result is None)
245        self.rent_config.set(&config_account.rent_config);
246
247        Ok(claim_result)
248    }
249}