solana_sdk/
rent_collector.rs

1#![cfg(feature = "full")]
2
3//! calculate and collect rent from Accounts
4use {
5    solana_account::{AccountSharedData, ReadableAccount, WritableAccount},
6    solana_sdk::{
7        clock::Epoch,
8        epoch_schedule::EpochSchedule,
9        genesis_config::GenesisConfig,
10        incinerator,
11        pubkey::Pubkey,
12        rent::{Rent, RentDue},
13    },
14};
15
16#[cfg_attr(feature = "frozen-abi", derive(AbiExample))]
17#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
18pub struct RentCollector {
19    pub epoch: Epoch,
20    pub epoch_schedule: EpochSchedule,
21    pub slots_per_year: f64,
22    pub rent: Rent,
23}
24
25impl Default for RentCollector {
26    fn default() -> Self {
27        Self {
28            epoch: Epoch::default(),
29            epoch_schedule: EpochSchedule::default(),
30            // derive default value using GenesisConfig::default()
31            slots_per_year: GenesisConfig::default().slots_per_year(),
32            rent: Rent::default(),
33        }
34    }
35}
36
37/// When rent is collected from an exempt account, rent_epoch is set to this
38/// value. The idea is to have a fixed, consistent value for rent_epoch for all accounts that do not collect rent.
39/// This enables us to get rid of the field completely.
40pub const RENT_EXEMPT_RENT_EPOCH: Epoch = Epoch::MAX;
41
42/// when rent is collected for this account, this is the action to apply to the account
43#[derive(Debug)]
44enum RentResult {
45    /// this account will never have rent collected from it
46    Exempt,
47    /// maybe we collect rent later, but not now
48    NoRentCollectionNow,
49    /// collect rent
50    CollectRent {
51        new_rent_epoch: Epoch,
52        rent_due: u64, // lamports, could be 0
53    },
54}
55
56impl RentCollector {
57    pub fn new(
58        epoch: Epoch,
59        epoch_schedule: EpochSchedule,
60        slots_per_year: f64,
61        rent: Rent,
62    ) -> Self {
63        Self {
64            epoch,
65            epoch_schedule,
66            slots_per_year,
67            rent,
68        }
69    }
70
71    pub fn clone_with_epoch(&self, epoch: Epoch) -> Self {
72        Self {
73            epoch,
74            ..self.clone()
75        }
76    }
77
78    /// true if it is easy to determine this account should consider having rent collected from it
79    pub fn should_collect_rent(&self, address: &Pubkey, executable: bool) -> bool {
80        !(executable // executable accounts must be rent-exempt balance
81            || *address == incinerator::id())
82    }
83
84    /// given an account that 'should_collect_rent'
85    /// returns (amount rent due, is_exempt_from_rent)
86    pub fn get_rent_due(
87        &self,
88        lamports: u64,
89        data_len: usize,
90        account_rent_epoch: Epoch,
91    ) -> RentDue {
92        if self.rent.is_exempt(lamports, data_len) {
93            RentDue::Exempt
94        } else {
95            let slots_elapsed: u64 = (account_rent_epoch..=self.epoch)
96                .map(|epoch| {
97                    self.epoch_schedule
98                        .get_slots_in_epoch(epoch.saturating_add(1))
99                })
100                .sum();
101
102            // avoid infinite rent in rust 1.45
103            let years_elapsed = if self.slots_per_year != 0.0 {
104                slots_elapsed as f64 / self.slots_per_year
105            } else {
106                0.0
107            };
108
109            // we know this account is not exempt
110            let due = self.rent.due_amount(data_len, years_elapsed);
111            RentDue::Paying(due)
112        }
113    }
114
115    // Updates the account's lamports and status, and returns the amount of rent collected, if any.
116    // This is NOT thread safe at some level. If we try to collect from the same account in
117    // parallel, we may collect twice.
118    #[must_use = "add to Bank::collected_rent"]
119    pub fn collect_from_existing_account(
120        &self,
121        address: &Pubkey,
122        account: &mut AccountSharedData,
123    ) -> CollectedInfo {
124        match self.calculate_rent_result(address, account) {
125            RentResult::Exempt => {
126                account.set_rent_epoch(RENT_EXEMPT_RENT_EPOCH);
127                CollectedInfo::default()
128            }
129            RentResult::NoRentCollectionNow => CollectedInfo::default(),
130            RentResult::CollectRent {
131                new_rent_epoch,
132                rent_due,
133            } => match account.lamports().checked_sub(rent_due) {
134                None | Some(0) => {
135                    let account = std::mem::take(account);
136                    CollectedInfo {
137                        rent_amount: account.lamports(),
138                        account_data_len_reclaimed: account.data().len() as u64,
139                    }
140                }
141                Some(lamports) => {
142                    account.set_lamports(lamports);
143                    account.set_rent_epoch(new_rent_epoch);
144                    CollectedInfo {
145                        rent_amount: rent_due,
146                        account_data_len_reclaimed: 0u64,
147                    }
148                }
149            },
150        }
151    }
152
153    /// determine what should happen to collect rent from this account
154    #[must_use]
155    fn calculate_rent_result(
156        &self,
157        address: &Pubkey,
158        account: &impl ReadableAccount,
159    ) -> RentResult {
160        if account.rent_epoch() == RENT_EXEMPT_RENT_EPOCH || account.rent_epoch() > self.epoch {
161            // potentially rent paying account (or known and already marked exempt)
162            // Maybe collect rent later, leave account alone for now.
163            return RentResult::NoRentCollectionNow;
164        }
165        if !self.should_collect_rent(address, account.executable()) {
166            // easy to determine this account should not consider having rent collected from it
167            return RentResult::Exempt;
168        }
169        match self.get_rent_due(
170            account.lamports(),
171            account.data().len(),
172            account.rent_epoch(),
173        ) {
174            // account will not have rent collected ever
175            RentDue::Exempt => RentResult::Exempt,
176            // potentially rent paying account
177            // Maybe collect rent later, leave account alone for now.
178            RentDue::Paying(0) => RentResult::NoRentCollectionNow,
179            // Rent is collected for next epoch.
180            RentDue::Paying(rent_due) => RentResult::CollectRent {
181                new_rent_epoch: self.epoch.saturating_add(1),
182                rent_due,
183            },
184        }
185    }
186}
187
188/// Information computed during rent collection
189#[derive(Debug, Default, Copy, Clone, Eq, PartialEq)]
190pub struct CollectedInfo {
191    /// Amount of rent collected from account
192    pub rent_amount: u64,
193    /// Size of data reclaimed from account (happens when account's lamports go to zero)
194    pub account_data_len_reclaimed: u64,
195}
196
197impl std::ops::Add for CollectedInfo {
198    type Output = Self;
199    fn add(self, other: Self) -> Self {
200        Self {
201            rent_amount: self.rent_amount.saturating_add(other.rent_amount),
202            account_data_len_reclaimed: self
203                .account_data_len_reclaimed
204                .saturating_add(other.account_data_len_reclaimed),
205        }
206    }
207}
208
209impl std::ops::AddAssign for CollectedInfo {
210    #![allow(clippy::arithmetic_side_effects)]
211    fn add_assign(&mut self, other: Self) {
212        *self = *self + other;
213    }
214}
215
216#[cfg(test)]
217mod tests {
218    use {super::*, assert_matches::assert_matches, solana_account::Account, solana_sdk::sysvar};
219
220    fn default_rent_collector_clone_with_epoch(epoch: Epoch) -> RentCollector {
221        RentCollector::default().clone_with_epoch(epoch)
222    }
223
224    impl RentCollector {
225        #[must_use = "add to Bank::collected_rent"]
226        fn collect_from_created_account(
227            &self,
228            address: &Pubkey,
229            account: &mut AccountSharedData,
230        ) -> CollectedInfo {
231            // initialize rent_epoch as created at this epoch
232            account.set_rent_epoch(self.epoch);
233            self.collect_from_existing_account(address, account)
234        }
235    }
236
237    #[test]
238    fn test_calculate_rent_result() {
239        let mut rent_collector = RentCollector::default();
240
241        let mut account = AccountSharedData::default();
242        assert_matches!(
243            rent_collector.calculate_rent_result(&Pubkey::default(), &account),
244            RentResult::NoRentCollectionNow
245        );
246        {
247            let mut account_clone = account.clone();
248            assert_eq!(
249                rent_collector
250                    .collect_from_existing_account(&Pubkey::default(), &mut account_clone),
251                CollectedInfo::default()
252            );
253            assert_eq!(account_clone, account);
254        }
255
256        account.set_executable(true);
257        assert_matches!(
258            rent_collector.calculate_rent_result(&Pubkey::default(), &account),
259            RentResult::Exempt
260        );
261        {
262            let mut account_clone = account.clone();
263            let mut account_expected = account.clone();
264            account_expected.set_rent_epoch(RENT_EXEMPT_RENT_EPOCH);
265            assert_eq!(
266                rent_collector
267                    .collect_from_existing_account(&Pubkey::default(), &mut account_clone),
268                CollectedInfo::default()
269            );
270            assert_eq!(account_clone, account_expected);
271        }
272
273        account.set_executable(false);
274        assert_matches!(
275            rent_collector.calculate_rent_result(&incinerator::id(), &account),
276            RentResult::Exempt
277        );
278        {
279            let mut account_clone = account.clone();
280            let mut account_expected = account.clone();
281            account_expected.set_rent_epoch(RENT_EXEMPT_RENT_EPOCH);
282            assert_eq!(
283                rent_collector
284                    .collect_from_existing_account(&incinerator::id(), &mut account_clone),
285                CollectedInfo::default()
286            );
287            assert_eq!(account_clone, account_expected);
288        }
289
290        // try a few combinations of rent collector rent epoch and collecting rent
291        for (rent_epoch, rent_due_expected) in [(2, 2), (3, 5)] {
292            rent_collector.epoch = rent_epoch;
293            account.set_lamports(10);
294            account.set_rent_epoch(1);
295            let new_rent_epoch_expected = rent_collector.epoch + 1;
296            assert!(
297                matches!(
298                    rent_collector.calculate_rent_result(&Pubkey::default(), &account),
299                    RentResult::CollectRent{ new_rent_epoch, rent_due} if new_rent_epoch == new_rent_epoch_expected && rent_due == rent_due_expected,
300                ),
301                "{:?}",
302                rent_collector.calculate_rent_result(&Pubkey::default(), &account)
303            );
304
305            {
306                let mut account_clone = account.clone();
307                assert_eq!(
308                    rent_collector
309                        .collect_from_existing_account(&Pubkey::default(), &mut account_clone),
310                    CollectedInfo {
311                        rent_amount: rent_due_expected,
312                        account_data_len_reclaimed: 0
313                    }
314                );
315                let mut account_expected = account.clone();
316                account_expected.set_lamports(account.lamports() - rent_due_expected);
317                account_expected.set_rent_epoch(new_rent_epoch_expected);
318                assert_eq!(account_clone, account_expected);
319            }
320        }
321
322        // enough lamports to make us exempt
323        account.set_lamports(1_000_000);
324        let result = rent_collector.calculate_rent_result(&Pubkey::default(), &account);
325        assert!(matches!(result, RentResult::Exempt), "{result:?}",);
326        {
327            let mut account_clone = account.clone();
328            let mut account_expected = account.clone();
329            account_expected.set_rent_epoch(RENT_EXEMPT_RENT_EPOCH);
330            assert_eq!(
331                rent_collector
332                    .collect_from_existing_account(&Pubkey::default(), &mut account_clone),
333                CollectedInfo::default()
334            );
335            assert_eq!(account_clone, account_expected);
336        }
337
338        // enough lamports to make us exempt
339        // but, our rent_epoch is set in the future, so we can't know if we are exempt yet or not.
340        // We don't calculate rent amount vs data if the rent_epoch is already in the future.
341        account.set_rent_epoch(1_000_000);
342        assert_matches!(
343            rent_collector.calculate_rent_result(&Pubkey::default(), &account),
344            RentResult::NoRentCollectionNow
345        );
346        {
347            let mut account_clone = account.clone();
348            assert_eq!(
349                rent_collector
350                    .collect_from_existing_account(&Pubkey::default(), &mut account_clone),
351                CollectedInfo::default()
352            );
353            assert_eq!(account_clone, account);
354        }
355    }
356
357    #[test]
358    fn test_collect_from_account_created_and_existing() {
359        let old_lamports = 1000;
360        let old_epoch = 1;
361        let new_epoch = 2;
362
363        let (mut created_account, mut existing_account) = {
364            let account = AccountSharedData::from(Account {
365                lamports: old_lamports,
366                rent_epoch: old_epoch,
367                ..Account::default()
368            });
369
370            (account.clone(), account)
371        };
372
373        let rent_collector = default_rent_collector_clone_with_epoch(new_epoch);
374
375        // collect rent on a newly-created account
376        let collected = rent_collector
377            .collect_from_created_account(&solana_sdk::pubkey::new_rand(), &mut created_account);
378        assert!(created_account.lamports() < old_lamports);
379        assert_eq!(
380            created_account.lamports() + collected.rent_amount,
381            old_lamports
382        );
383        assert_ne!(created_account.rent_epoch(), old_epoch);
384        assert_eq!(collected.account_data_len_reclaimed, 0);
385
386        // collect rent on a already-existing account
387        let collected = rent_collector
388            .collect_from_existing_account(&solana_sdk::pubkey::new_rand(), &mut existing_account);
389        assert!(existing_account.lamports() < old_lamports);
390        assert_eq!(
391            existing_account.lamports() + collected.rent_amount,
392            old_lamports
393        );
394        assert_ne!(existing_account.rent_epoch(), old_epoch);
395        assert_eq!(collected.account_data_len_reclaimed, 0);
396
397        // newly created account should be collected for less rent; thus more remaining balance
398        assert!(created_account.lamports() > existing_account.lamports());
399        assert_eq!(created_account.rent_epoch(), existing_account.rent_epoch());
400    }
401
402    #[test]
403    fn test_rent_exempt_temporal_escape() {
404        for pass in 0..2 {
405            let mut account = AccountSharedData::default();
406            let epoch = 3;
407            let huge_lamports = 123_456_789_012;
408            let tiny_lamports = 789_012;
409            let pubkey = solana_sdk::pubkey::new_rand();
410
411            assert_eq!(account.rent_epoch(), 0);
412
413            // create a tested rent collector
414            let rent_collector = default_rent_collector_clone_with_epoch(epoch);
415
416            if pass == 0 {
417                account.set_lamports(huge_lamports);
418                // first mark account as being collected while being rent-exempt
419                let collected = rent_collector.collect_from_existing_account(&pubkey, &mut account);
420                assert_eq!(account.lamports(), huge_lamports);
421                assert_eq!(collected, CollectedInfo::default());
422                continue;
423            }
424
425            // decrease the balance not to be rent-exempt
426            // In a real validator, it is not legal to reduce an account's lamports such that the account becomes rent paying.
427            // So, pass == 0 above tests the case of rent that is exempt. pass == 1 tests the case where we are rent paying.
428            account.set_lamports(tiny_lamports);
429
430            // ... and trigger another rent collection on the same epoch and check that rent is working
431            let collected = rent_collector.collect_from_existing_account(&pubkey, &mut account);
432            assert_eq!(account.lamports(), tiny_lamports - collected.rent_amount);
433            assert_ne!(collected, CollectedInfo::default());
434        }
435    }
436
437    #[test]
438    fn test_rent_exempt_sysvar() {
439        let tiny_lamports = 1;
440        let mut account = AccountSharedData::default();
441        account.set_owner(sysvar::id());
442        account.set_lamports(tiny_lamports);
443
444        let pubkey = solana_sdk::pubkey::new_rand();
445
446        assert_eq!(account.rent_epoch(), 0);
447
448        let epoch = 3;
449        let rent_collector = default_rent_collector_clone_with_epoch(epoch);
450
451        let collected = rent_collector.collect_from_existing_account(&pubkey, &mut account);
452        assert_eq!(account.lamports(), 0);
453        assert_eq!(collected.rent_amount, 1);
454    }
455
456    /// Ensure that when an account is "rent collected" away, its data len is returned.
457    #[test]
458    fn test_collect_cleans_up_account() {
459        solana_logger::setup();
460        let account_lamports = 1; // must be *below* rent amount
461        let account_data_len = 567;
462        let account_rent_epoch = 11;
463        let mut account = AccountSharedData::from(Account {
464            lamports: account_lamports, // <-- must be below rent-exempt amount
465            data: vec![u8::default(); account_data_len],
466            rent_epoch: account_rent_epoch,
467            ..Account::default()
468        });
469        let rent_collector = default_rent_collector_clone_with_epoch(account_rent_epoch + 1);
470
471        let collected =
472            rent_collector.collect_from_existing_account(&Pubkey::new_unique(), &mut account);
473
474        assert_eq!(collected.rent_amount, account_lamports);
475        assert_eq!(
476            collected.account_data_len_reclaimed,
477            account_data_len as u64
478        );
479        assert_eq!(account, AccountSharedData::default());
480    }
481}