solana_runtime/
rent_collector.rs

1//! calculate and collect rent from Accounts
2use {
3    log::*,
4    solana_sdk::{
5        account::{AccountSharedData, ReadableAccount, WritableAccount},
6        clock::Epoch,
7        epoch_schedule::EpochSchedule,
8        genesis_config::GenesisConfig,
9        incinerator,
10        pubkey::Pubkey,
11        rent::{Rent, RentDue},
12    },
13};
14
15#[derive(Serialize, Deserialize, Clone, PartialEq, Debug, AbiExample)]
16pub struct RentCollector {
17    pub epoch: Epoch,
18    pub epoch_schedule: EpochSchedule,
19    pub slots_per_year: f64,
20    pub rent: Rent,
21}
22
23impl Default for RentCollector {
24    fn default() -> Self {
25        Self {
26            epoch: Epoch::default(),
27            epoch_schedule: EpochSchedule::default(),
28            // derive default value using GenesisConfig::default()
29            slots_per_year: GenesisConfig::default().slots_per_year(),
30            rent: Rent::default(),
31        }
32    }
33}
34
35/// when rent is collected for this account, this is the action to apply to the account
36#[derive(Debug)]
37pub(crate) enum RentResult {
38    /// maybe collect rent later, leave account alone
39    LeaveAloneNoRent,
40    /// collect rent
41    CollectRent {
42        new_rent_epoch: Epoch,
43        rent_due: u64, // lamports
44    },
45}
46
47impl RentCollector {
48    pub(crate) fn new(
49        epoch: Epoch,
50        epoch_schedule: EpochSchedule,
51        slots_per_year: f64,
52        rent: Rent,
53    ) -> Self {
54        Self {
55            epoch,
56            epoch_schedule,
57            slots_per_year,
58            rent,
59        }
60    }
61
62    pub(crate) fn clone_with_epoch(&self, epoch: Epoch) -> Self {
63        Self {
64            epoch,
65            ..self.clone()
66        }
67    }
68
69    /// true if it is easy to determine this account should consider having rent collected from it
70    pub(crate) fn should_collect_rent(
71        &self,
72        address: &Pubkey,
73        account: &impl ReadableAccount,
74    ) -> bool {
75        !(account.executable() // executable accounts must be rent-exempt balance
76            || *address == incinerator::id())
77    }
78
79    /// given an account that 'should_collect_rent'
80    /// returns (amount rent due, is_exempt_from_rent)
81    pub(crate) fn get_rent_due(&self, account: &impl ReadableAccount) -> RentDue {
82        if self
83            .rent
84            .is_exempt(account.lamports(), account.data().len())
85        {
86            RentDue::Exempt
87        } else {
88            let account_rent_epoch = account.rent_epoch();
89            let slots_elapsed: u64 = (account_rent_epoch..=self.epoch)
90                .map(|epoch| self.epoch_schedule.get_slots_in_epoch(epoch + 1))
91                .sum();
92
93            // avoid infinite rent in rust 1.45
94            let years_elapsed = if self.slots_per_year != 0.0 {
95                slots_elapsed as f64 / self.slots_per_year
96            } else {
97                0.0
98            };
99
100            // we know this account is not exempt
101            let due = self.rent.due_amount(account.data().len(), years_elapsed);
102
103            // we expect rent_epoch to always be one of: {0, self.epoch-1, self.epoch, self.epoch+1}
104            if account_rent_epoch != 0
105                && (account_rent_epoch + 1 < self.epoch || account_rent_epoch > self.epoch + 1)
106            {
107                // this should not occur in a running validator
108                if due == 0 {
109                    inc_new_counter_info!("rent-collector-rent-epoch-range-large-exempt", 1);
110                } else {
111                    inc_new_counter_info!("rent-collector-rent-epoch-range-large-paying", 1);
112                }
113            }
114
115            RentDue::Paying(due)
116        }
117    }
118
119    // Updates the account's lamports and status, and returns the amount of rent collected, if any.
120    // This is NOT thread safe at some level. If we try to collect from the same account in
121    // parallel, we may collect twice.
122    #[must_use = "add to Bank::collected_rent"]
123    pub(crate) fn collect_from_existing_account(
124        &self,
125        address: &Pubkey,
126        account: &mut AccountSharedData,
127        filler_account_suffix: Option<&Pubkey>,
128        preserve_rent_epoch_for_rent_exempt_accounts: bool,
129    ) -> CollectedInfo {
130        match self.calculate_rent_result(
131            address,
132            account,
133            filler_account_suffix,
134            preserve_rent_epoch_for_rent_exempt_accounts,
135        ) {
136            RentResult::LeaveAloneNoRent => CollectedInfo::default(),
137            RentResult::CollectRent {
138                new_rent_epoch,
139                rent_due,
140            } => match account.lamports().checked_sub(rent_due) {
141                None | Some(0) => {
142                    let account = std::mem::take(account);
143                    CollectedInfo {
144                        rent_amount: account.lamports(),
145                        account_data_len_reclaimed: account.data().len() as u64,
146                    }
147                }
148                Some(lamports) => {
149                    account.set_lamports(lamports);
150                    account.set_rent_epoch(new_rent_epoch);
151                    CollectedInfo {
152                        rent_amount: rent_due,
153                        account_data_len_reclaimed: 0u64,
154                    }
155                }
156            },
157        }
158    }
159
160    /// determine what should happen to collect rent from this account
161    #[must_use]
162    pub(crate) fn calculate_rent_result(
163        &self,
164        address: &Pubkey,
165        account: &impl ReadableAccount,
166        filler_account_suffix: Option<&Pubkey>,
167        preserve_rent_epoch_for_rent_exempt_accounts: bool,
168    ) -> RentResult {
169        if self.can_skip_rent_collection(address, account, filler_account_suffix) {
170            return RentResult::LeaveAloneNoRent;
171        }
172        match self.get_rent_due(account) {
173            // Rent isn't collected for the next epoch.
174            // Make sure to check exempt status again later in current epoch.
175            RentDue::Exempt => {
176                if preserve_rent_epoch_for_rent_exempt_accounts {
177                    RentResult::LeaveAloneNoRent
178                } else {
179                    RentResult::CollectRent {
180                        new_rent_epoch: self.epoch,
181                        rent_due: 0,
182                    }
183                }
184            }
185            // Maybe collect rent later, leave account alone.
186            RentDue::Paying(0) => RentResult::LeaveAloneNoRent,
187            // Rent is collected for next epoch.
188            RentDue::Paying(rent_due) => RentResult::CollectRent {
189                new_rent_epoch: self.epoch + 1,
190                rent_due,
191            },
192        }
193    }
194
195    #[must_use = "add to Bank::collected_rent"]
196    pub(crate) fn collect_from_created_account(
197        &self,
198        address: &Pubkey,
199        account: &mut AccountSharedData,
200        preserve_rent_epoch_for_rent_exempt_accounts: bool,
201    ) -> CollectedInfo {
202        // initialize rent_epoch as created at this epoch
203        account.set_rent_epoch(self.epoch);
204        self.collect_from_existing_account(
205            address,
206            account,
207            None, // filler_account_suffix
208            preserve_rent_epoch_for_rent_exempt_accounts,
209        )
210    }
211
212    /// Performs easy checks to see if rent collection can be skipped
213    fn can_skip_rent_collection(
214        &self,
215        address: &Pubkey,
216        account: &impl ReadableAccount,
217        filler_account_suffix: Option<&Pubkey>,
218    ) -> bool {
219        !self.should_collect_rent(address, account)
220            || account.rent_epoch() > self.epoch
221            || crate::accounts_db::AccountsDb::is_filler_account_helper(
222                address,
223                filler_account_suffix,
224            )
225    }
226}
227
228/// Information computed during rent collection
229#[derive(Debug, Default, Copy, Clone, Eq, PartialEq)]
230pub(crate) struct CollectedInfo {
231    /// Amount of rent collected from account
232    pub(crate) rent_amount: u64,
233    /// Size of data reclaimed from account (happens when account's lamports go to zero)
234    pub(crate) account_data_len_reclaimed: u64,
235}
236
237impl std::ops::Add for CollectedInfo {
238    type Output = Self;
239    fn add(self, other: Self) -> Self {
240        Self {
241            rent_amount: self.rent_amount + other.rent_amount,
242            account_data_len_reclaimed: self.account_data_len_reclaimed
243                + other.account_data_len_reclaimed,
244        }
245    }
246}
247
248impl std::ops::AddAssign for CollectedInfo {
249    fn add_assign(&mut self, other: Self) {
250        *self = *self + other;
251    }
252}
253
254#[cfg(test)]
255mod tests {
256    use {
257        super::*,
258        solana_sdk::{account::Account, sysvar},
259    };
260
261    fn default_rent_collector_clone_with_epoch(epoch: Epoch) -> RentCollector {
262        RentCollector::default().clone_with_epoch(epoch)
263    }
264
265    #[test]
266    fn test_collect_from_account_created_and_existing() {
267        let old_lamports = 1000;
268        let old_epoch = 1;
269        let new_epoch = 2;
270
271        let (mut created_account, mut existing_account) = {
272            let account = AccountSharedData::from(Account {
273                lamports: old_lamports,
274                rent_epoch: old_epoch,
275                ..Account::default()
276            });
277
278            (account.clone(), account)
279        };
280
281        let rent_collector = default_rent_collector_clone_with_epoch(new_epoch);
282
283        // collect rent on a newly-created account
284        let collected = rent_collector.collect_from_created_account(
285            &solana_sdk::pubkey::new_rand(),
286            &mut created_account,
287            true, // preserve_rent_epoch_for_rent_exempt_accounts
288        );
289        assert!(created_account.lamports() < old_lamports);
290        assert_eq!(
291            created_account.lamports() + collected.rent_amount,
292            old_lamports
293        );
294        assert_ne!(created_account.rent_epoch(), old_epoch);
295        assert_eq!(collected.account_data_len_reclaimed, 0);
296
297        // collect rent on a already-existing account
298        let collected = rent_collector.collect_from_existing_account(
299            &solana_sdk::pubkey::new_rand(),
300            &mut existing_account,
301            None, // filler_account_suffix
302            true, // preserve_rent_epoch_for_rent_exempt_accounts
303        );
304        assert!(existing_account.lamports() < old_lamports);
305        assert_eq!(
306            existing_account.lamports() + collected.rent_amount,
307            old_lamports
308        );
309        assert_ne!(existing_account.rent_epoch(), old_epoch);
310        assert_eq!(collected.account_data_len_reclaimed, 0);
311
312        // newly created account should be collected for less rent; thus more remaining balance
313        assert!(created_account.lamports() > existing_account.lamports());
314        assert_eq!(created_account.rent_epoch(), existing_account.rent_epoch());
315    }
316
317    #[test]
318    fn test_rent_exempt_temporal_escape() {
319        let mut account = AccountSharedData::default();
320        let epoch = 3;
321        let huge_lamports = 123_456_789_012;
322        let tiny_lamports = 789_012;
323        let pubkey = solana_sdk::pubkey::new_rand();
324
325        account.set_lamports(huge_lamports);
326        assert_eq!(account.rent_epoch(), 0);
327
328        // create a tested rent collector
329        let rent_collector = default_rent_collector_clone_with_epoch(epoch);
330
331        // first mark account as being collected while being rent-exempt
332        let collected = rent_collector.collect_from_existing_account(
333            &pubkey,
334            &mut account,
335            None, // filler_account_suffix
336            true, // preserve_rent_epoch_for_rent_exempt_accounts
337        );
338        assert_eq!(account.lamports(), huge_lamports);
339        assert_eq!(collected, CollectedInfo::default());
340
341        // decrease the balance not to be rent-exempt
342        account.set_lamports(tiny_lamports);
343
344        // ... and trigger another rent collection on the same epoch and check that rent is working
345        let collected = rent_collector.collect_from_existing_account(
346            &pubkey,
347            &mut account,
348            None, // filler_account_suffix
349            true, // preserve_rent_epoch_for_rent_exempt_accounts
350        );
351        assert_eq!(account.lamports(), tiny_lamports - collected.rent_amount);
352        assert_ne!(collected, CollectedInfo::default());
353    }
354
355    #[test]
356    fn test_rent_exempt_sysvar() {
357        let tiny_lamports = 1;
358        let mut account = AccountSharedData::default();
359        account.set_owner(sysvar::id());
360        account.set_lamports(tiny_lamports);
361
362        let pubkey = solana_sdk::pubkey::new_rand();
363
364        assert_eq!(account.rent_epoch(), 0);
365
366        let epoch = 3;
367        let rent_collector = default_rent_collector_clone_with_epoch(epoch);
368
369        let collected = rent_collector.collect_from_existing_account(
370            &pubkey,
371            &mut account,
372            None, // filler_account_suffix
373            true, // preserve_rent_epoch_for_rent_exempt_accounts
374        );
375        assert_eq!(account.lamports(), 0);
376        assert_eq!(collected.rent_amount, 1);
377    }
378
379    /// Ensure that when an account is "rent collected" away, its data len is returned.
380    #[test]
381    fn test_collect_cleans_up_account() {
382        solana_logger::setup();
383        let account_lamports = 1; // must be *below* rent amount
384        let account_data_len = 567;
385        let account_rent_epoch = 11;
386        let mut account = AccountSharedData::from(Account {
387            lamports: account_lamports, // <-- must be below rent-exempt amount
388            data: vec![u8::default(); account_data_len],
389            rent_epoch: account_rent_epoch,
390            ..Account::default()
391        });
392        let rent_collector = default_rent_collector_clone_with_epoch(account_rent_epoch + 1);
393
394        let collected = rent_collector.collect_from_existing_account(
395            &Pubkey::new_unique(),
396            &mut account,
397            None, // filler_account_suffix
398            true, // preserve_rent_epoch_for_rent_exempt_accounts
399        );
400
401        assert_eq!(collected.rent_amount, account_lamports);
402        assert_eq!(
403            collected.account_data_len_reclaimed,
404            account_data_len as u64
405        );
406        assert_eq!(account, AccountSharedData::default());
407    }
408}