solana/runtime/src/rent_collector.rs

229 lines
7.7 KiB
Rust
Raw Normal View History

//! calculate and collect rent from Accounts
use solana_sdk::{
account::{AccountSharedData, ReadableAccount, WritableAccount},
2021-03-09 14:31:33 -08:00
clock::Epoch,
epoch_schedule::EpochSchedule,
genesis_config::GenesisConfig,
incinerator,
pubkey::Pubkey,
rent::Rent,
sysvar,
};
#[derive(Serialize, Deserialize, Clone, PartialEq, Debug, AbiExample)]
pub struct RentCollector {
pub epoch: Epoch,
pub epoch_schedule: EpochSchedule,
pub slots_per_year: f64,
2019-10-30 16:25:12 -07:00
pub rent: Rent,
}
impl Default for RentCollector {
fn default() -> Self {
Self {
epoch: Epoch::default(),
epoch_schedule: EpochSchedule::default(),
// derive default value using GenesisConfig::default()
slots_per_year: GenesisConfig::default().slots_per_year(),
rent: Rent::default(),
}
}
}
impl RentCollector {
pub fn new(
epoch: Epoch,
epoch_schedule: &EpochSchedule,
slots_per_year: f64,
2019-10-30 16:25:12 -07:00
rent: &Rent,
) -> Self {
Self {
epoch,
epoch_schedule: *epoch_schedule,
slots_per_year,
2019-10-30 16:25:12 -07:00
rent: *rent,
}
}
pub fn clone_with_epoch(&self, epoch: Epoch) -> Self {
Self {
epoch,
..self.clone()
}
}
// updates this account's lamports and status and returns
// the account rent collected, if any
2021-09-08 17:25:20 -07:00
// This is NOT thread safe at some level. If we try to collect from the same account in parallel, we may collect twice.
#[must_use = "add to Bank::collected_rent"]
2021-03-09 13:06:07 -08:00
pub fn collect_from_existing_account(
&self,
address: &Pubkey,
account: &mut AccountSharedData,
rent_for_sysvars: bool,
2021-03-09 13:06:07 -08:00
) -> u64 {
if account.executable() // executable accounts must be rent-exempt balance
|| account.rent_epoch() > self.epoch
2021-07-01 00:12:55 -07:00
|| (!rent_for_sysvars && sysvar::check_id(account.owner()))
2020-04-30 22:04:08 -07:00
|| *address == incinerator::id()
{
0
} else {
let slots_elapsed: u64 = (account.rent_epoch()..=self.epoch)
.map(|epoch| self.epoch_schedule.get_slots_in_epoch(epoch + 1))
.sum();
// avoid infinite rent in rust 1.45
let years_elapsed = if self.slots_per_year != 0.0 {
slots_elapsed as f64 / self.slots_per_year
} else {
0.0
};
let (rent_due, exempt) =
self.rent
2021-04-29 08:44:46 -07:00
.due(account.lamports(), account.data().len(), years_elapsed);
if exempt || rent_due != 0 {
if account.lamports() > rent_due {
account.set_rent_epoch(
self.epoch
+ if exempt {
// Rent isn't collected for the next epoch
// Make sure to check exempt status later in current epoch again
0
} else {
// Rent is collected for next epoch
1
},
);
let _ = account.checked_sub_lamports(rent_due); // will not fail. We check above.
rent_due
} else {
2021-04-29 08:44:46 -07:00
let rent_charged = account.lamports();
2021-03-09 13:06:07 -08:00
*account = AccountSharedData::default();
rent_charged
}
} else {
// maybe collect rent later, leave account alone
0
}
}
}
#[must_use = "add to Bank::collected_rent"]
2021-03-09 13:06:07 -08:00
pub fn collect_from_created_account(
&self,
address: &Pubkey,
account: &mut AccountSharedData,
rent_for_sysvars: bool,
2021-03-09 13:06:07 -08:00
) -> u64 {
// initialize rent_epoch as created at this epoch
account.set_rent_epoch(self.epoch);
self.collect_from_existing_account(address, account, rent_for_sysvars)
}
}
#[cfg(test)]
mod tests {
use super::*;
use solana_sdk::account::Account;
#[test]
fn test_collect_from_account_created_and_existing() {
let old_lamports = 1000;
let old_epoch = 1;
let new_epoch = 3;
let (mut created_account, mut existing_account) = {
let account = AccountSharedData::from(Account {
2020-12-13 17:26:34 -08:00
lamports: old_lamports,
rent_epoch: old_epoch,
..Account::default()
});
(account.clone(), account)
};
let rent_collector = RentCollector::default().clone_with_epoch(new_epoch);
// collect rent on a newly-created account
let collected = rent_collector.collect_from_created_account(
&solana_sdk::pubkey::new_rand(),
&mut created_account,
true,
);
2021-05-03 08:45:54 -07:00
assert!(created_account.lamports() < old_lamports);
assert_eq!(created_account.lamports() + collected, old_lamports);
assert_ne!(created_account.rent_epoch(), old_epoch);
// collect rent on a already-existing account
let collected = rent_collector.collect_from_existing_account(
&solana_sdk::pubkey::new_rand(),
&mut existing_account,
true,
);
2021-05-03 08:45:54 -07:00
assert!(existing_account.lamports() < old_lamports);
assert_eq!(existing_account.lamports() + collected, old_lamports);
assert_ne!(existing_account.rent_epoch(), old_epoch);
// newly created account should be collected for less rent; thus more remaining balance
2021-05-03 08:45:54 -07:00
assert!(created_account.lamports() > existing_account.lamports());
assert_eq!(created_account.rent_epoch(), existing_account.rent_epoch());
}
#[test]
fn test_rent_exempt_temporal_escape() {
2021-03-09 13:06:07 -08:00
let mut account = AccountSharedData::default();
let epoch = 3;
let huge_lamports = 123_456_789_012;
let tiny_lamports = 789_012;
let mut collected;
let pubkey = solana_sdk::pubkey::new_rand();
2021-04-30 11:20:54 -07:00
account.set_lamports(huge_lamports);
assert_eq!(account.rent_epoch(), 0);
// create a tested rent collector
let rent_collector = RentCollector::default().clone_with_epoch(epoch);
// first mark account as being collected while being rent-exempt
collected = rent_collector.collect_from_existing_account(&pubkey, &mut account, true);
2021-05-03 08:45:54 -07:00
assert_eq!(account.lamports(), huge_lamports);
assert_eq!(collected, 0);
// decrease the balance not to be rent-exempt
2021-04-30 11:20:54 -07:00
account.set_lamports(tiny_lamports);
// ... and trigger another rent collection on the same epoch and check that rent is working
collected = rent_collector.collect_from_existing_account(&pubkey, &mut account, true);
2021-05-03 08:45:54 -07:00
assert_eq!(account.lamports(), tiny_lamports - collected);
assert_ne!(collected, 0);
}
#[test]
fn test_rent_exempt_sysvar() {
let tiny_lamports = 1;
let mut account = AccountSharedData::default();
account.set_owner(sysvar::id());
account.set_lamports(tiny_lamports);
let pubkey = solana_sdk::pubkey::new_rand();
assert_eq!(account.rent_epoch(), 0);
let epoch = 3;
let rent_collector = RentCollector::default().clone_with_epoch(epoch);
// old behavior: sysvars are special-cased
let collected = rent_collector.collect_from_existing_account(&pubkey, &mut account, false);
assert_eq!(account.lamports(), tiny_lamports);
assert_eq!(collected, 0);
// new behavior: sysvars are NOT special-cased
let collected = rent_collector.collect_from_existing_account(&pubkey, &mut account, true);
assert_eq!(account.lamports(), 0);
assert_eq!(collected, 1);
}
}