1#![cfg(feature = "full")]
2
3use {
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 slots_per_year: GenesisConfig::default().slots_per_year(),
32 rent: Rent::default(),
33 }
34 }
35}
36
37pub const RENT_EXEMPT_RENT_EPOCH: Epoch = Epoch::MAX;
41
42#[derive(Debug)]
44enum RentResult {
45 Exempt,
47 NoRentCollectionNow,
49 CollectRent {
51 new_rent_epoch: Epoch,
52 rent_due: u64, },
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 pub fn should_collect_rent(&self, address: &Pubkey, executable: bool) -> bool {
80 !(executable || *address == incinerator::id())
82 }
83
84 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 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 let due = self.rent.due_amount(data_len, years_elapsed);
111 RentDue::Paying(due)
112 }
113 }
114
115 #[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 #[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 return RentResult::NoRentCollectionNow;
164 }
165 if !self.should_collect_rent(address, account.executable()) {
166 return RentResult::Exempt;
168 }
169 match self.get_rent_due(
170 account.lamports(),
171 account.data().len(),
172 account.rent_epoch(),
173 ) {
174 RentDue::Exempt => RentResult::Exempt,
176 RentDue::Paying(0) => RentResult::NoRentCollectionNow,
179 RentDue::Paying(rent_due) => RentResult::CollectRent {
181 new_rent_epoch: self.epoch.saturating_add(1),
182 rent_due,
183 },
184 }
185 }
186}
187
188#[derive(Debug, Default, Copy, Clone, Eq, PartialEq)]
190pub struct CollectedInfo {
191 pub rent_amount: u64,
193 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 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 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 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 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 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 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 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 let rent_collector = default_rent_collector_clone_with_epoch(epoch);
415
416 if pass == 0 {
417 account.set_lamports(huge_lamports);
418 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 account.set_lamports(tiny_lamports);
429
430 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 #[test]
458 fn test_collect_cleans_up_account() {
459 solana_logger::setup();
460 let account_lamports = 1; let account_data_len = 567;
462 let account_rent_epoch = 11;
463 let mut account = AccountSharedData::from(Account {
464 lamports: account_lamports, 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}