1use {
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 slots_per_year: GenesisConfig::default().slots_per_year(),
30 rent: Rent::default(),
31 }
32 }
33}
34
35#[derive(Debug)]
37pub(crate) enum RentResult {
38 LeaveAloneNoRent,
40 CollectRent {
42 new_rent_epoch: Epoch,
43 rent_due: u64, },
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 pub(crate) fn should_collect_rent(
71 &self,
72 address: &Pubkey,
73 account: &impl ReadableAccount,
74 ) -> bool {
75 !(account.executable() || *address == incinerator::id())
77 }
78
79 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 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 let due = self.rent.due_amount(account.data().len(), years_elapsed);
102
103 if account_rent_epoch != 0
105 && (account_rent_epoch + 1 < self.epoch || account_rent_epoch > self.epoch + 1)
106 {
107 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 #[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 #[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 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 RentDue::Paying(0) => RentResult::LeaveAloneNoRent,
187 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 account.set_rent_epoch(self.epoch);
204 self.collect_from_existing_account(
205 address,
206 account,
207 None, preserve_rent_epoch_for_rent_exempt_accounts,
209 )
210 }
211
212 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#[derive(Debug, Default, Copy, Clone, Eq, PartialEq)]
230pub(crate) struct CollectedInfo {
231 pub(crate) rent_amount: u64,
233 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 let collected = rent_collector.collect_from_created_account(
285 &solana_sdk::pubkey::new_rand(),
286 &mut created_account,
287 true, );
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 let collected = rent_collector.collect_from_existing_account(
299 &solana_sdk::pubkey::new_rand(),
300 &mut existing_account,
301 None, true, );
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 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 let rent_collector = default_rent_collector_clone_with_epoch(epoch);
330
331 let collected = rent_collector.collect_from_existing_account(
333 &pubkey,
334 &mut account,
335 None, true, );
338 assert_eq!(account.lamports(), huge_lamports);
339 assert_eq!(collected, CollectedInfo::default());
340
341 account.set_lamports(tiny_lamports);
343
344 let collected = rent_collector.collect_from_existing_account(
346 &pubkey,
347 &mut account,
348 None, true, );
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, true, );
375 assert_eq!(account.lamports(), 0);
376 assert_eq!(collected.rent_amount, 1);
377 }
378
379 #[test]
381 fn test_collect_cleans_up_account() {
382 solana_logger::setup();
383 let account_lamports = 1; let account_data_len = 567;
385 let account_rent_epoch = 11;
386 let mut account = AccountSharedData::from(Account {
387 lamports: account_lamports, 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, true, );
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}