use solana_sdk::{
account::{AccountSharedData, ReadableAccount, WritableAccount},
clock::Epoch,
epoch_schedule::EpochSchedule,
genesis_config::GenesisConfig,
incinerator,
pubkey::Pubkey,
rent::{Rent, RentDue},
};
#[derive(Serialize, Deserialize, Clone, PartialEq, Debug, AbiExample)]
pub struct RentCollector {
pub epoch: Epoch,
pub epoch_schedule: EpochSchedule,
pub slots_per_year: f64,
pub rent: Rent,
}
impl Default for RentCollector {
fn default() -> Self {
Self {
epoch: Epoch::default(),
epoch_schedule: EpochSchedule::default(),
slots_per_year: GenesisConfig::default().slots_per_year(),
rent: Rent::default(),
}
}
}
pub const RENT_EXEMPT_RENT_EPOCH: Epoch = Epoch::MAX;
#[derive(Debug)]
enum RentResult {
Exempt,
NoRentCollectionNow,
CollectRent {
new_rent_epoch: Epoch,
rent_due: u64, },
}
impl RentCollector {
pub fn new(
epoch: Epoch,
epoch_schedule: EpochSchedule,
slots_per_year: f64,
rent: Rent,
) -> Self {
Self {
epoch,
epoch_schedule,
slots_per_year,
rent,
}
}
pub fn clone_with_epoch(&self, epoch: Epoch) -> Self {
Self {
epoch,
..self.clone()
}
}
pub fn should_collect_rent(&self, address: &Pubkey, account: &impl ReadableAccount) -> bool {
!(account.executable() || *address == incinerator::id())
}
pub fn get_rent_due(&self, account: &impl ReadableAccount) -> RentDue {
if self
.rent
.is_exempt(account.lamports(), account.data().len())
{
RentDue::Exempt
} else {
let account_rent_epoch = account.rent_epoch();
let slots_elapsed: u64 = (account_rent_epoch..=self.epoch)
.map(|epoch| self.epoch_schedule.get_slots_in_epoch(epoch + 1))
.sum();
let years_elapsed = if self.slots_per_year != 0.0 {
slots_elapsed as f64 / self.slots_per_year
} else {
0.0
};
let due = self.rent.due_amount(account.data().len(), years_elapsed);
RentDue::Paying(due)
}
}
#[must_use = "add to Bank::collected_rent"]
pub fn collect_from_existing_account(
&self,
address: &Pubkey,
account: &mut AccountSharedData,
set_exempt_rent_epoch_max: bool,
) -> CollectedInfo {
match self.calculate_rent_result(address, account) {
RentResult::Exempt => {
if set_exempt_rent_epoch_max {
account.set_rent_epoch(RENT_EXEMPT_RENT_EPOCH);
}
CollectedInfo::default()
}
RentResult::NoRentCollectionNow => CollectedInfo::default(),
RentResult::CollectRent {
new_rent_epoch,
rent_due,
} => match account.lamports().checked_sub(rent_due) {
None | Some(0) => {
let account = std::mem::take(account);
CollectedInfo {
rent_amount: account.lamports(),
account_data_len_reclaimed: account.data().len() as u64,
}
}
Some(lamports) => {
account.set_lamports(lamports);
account.set_rent_epoch(new_rent_epoch);
CollectedInfo {
rent_amount: rent_due,
account_data_len_reclaimed: 0u64,
}
}
},
}
}
#[must_use]
fn calculate_rent_result(
&self,
address: &Pubkey,
account: &impl ReadableAccount,
) -> RentResult {
if account.rent_epoch() == RENT_EXEMPT_RENT_EPOCH || account.rent_epoch() > self.epoch {
return RentResult::NoRentCollectionNow;
}
if !self.should_collect_rent(address, account) {
return RentResult::Exempt;
}
match self.get_rent_due(account) {
RentDue::Exempt => RentResult::Exempt,
RentDue::Paying(0) => RentResult::NoRentCollectionNow,
RentDue::Paying(rent_due) => RentResult::CollectRent {
new_rent_epoch: self.epoch + 1,
rent_due,
},
}
}
}
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq)]
pub struct CollectedInfo {
pub rent_amount: u64,
pub account_data_len_reclaimed: u64,
}
impl std::ops::Add for CollectedInfo {
type Output = Self;
fn add(self, other: Self) -> Self {
Self {
rent_amount: self.rent_amount + other.rent_amount,
account_data_len_reclaimed: self.account_data_len_reclaimed
+ other.account_data_len_reclaimed,
}
}
}
impl std::ops::AddAssign for CollectedInfo {
fn add_assign(&mut self, other: Self) {
*self = *self + other;
}
}
#[cfg(test)]
mod tests {
use {
super::*,
assert_matches::assert_matches,
solana_sdk::{account::Account, sysvar},
};
fn default_rent_collector_clone_with_epoch(epoch: Epoch) -> RentCollector {
RentCollector::default().clone_with_epoch(epoch)
}
impl RentCollector {
#[must_use = "add to Bank::collected_rent"]
fn collect_from_created_account(
&self,
address: &Pubkey,
account: &mut AccountSharedData,
set_exempt_rent_epoch_max: bool,
) -> CollectedInfo {
account.set_rent_epoch(self.epoch);
self.collect_from_existing_account(address, account, set_exempt_rent_epoch_max)
}
}
#[test]
fn test_calculate_rent_result() {
for set_exempt_rent_epoch_max in [false, true] {
let mut rent_collector = RentCollector::default();
let mut account = AccountSharedData::default();
assert_matches!(
rent_collector.calculate_rent_result(&Pubkey::default(), &account),
RentResult::NoRentCollectionNow
);
{
let mut account_clone = account.clone();
assert_eq!(
rent_collector.collect_from_existing_account(
&Pubkey::default(),
&mut account_clone,
set_exempt_rent_epoch_max
),
CollectedInfo::default()
);
assert_eq!(account_clone, account);
}
account.set_executable(true);
assert_matches!(
rent_collector.calculate_rent_result(&Pubkey::default(), &account),
RentResult::Exempt
);
{
let mut account_clone = account.clone();
let mut account_expected = account.clone();
if set_exempt_rent_epoch_max {
account_expected.set_rent_epoch(RENT_EXEMPT_RENT_EPOCH);
}
assert_eq!(
rent_collector.collect_from_existing_account(
&Pubkey::default(),
&mut account_clone,
set_exempt_rent_epoch_max
),
CollectedInfo::default()
);
assert_eq!(account_clone, account_expected);
}
account.set_executable(false);
assert_matches!(
rent_collector.calculate_rent_result(&incinerator::id(), &account),
RentResult::Exempt
);
{
let mut account_clone = account.clone();
let mut account_expected = account.clone();
if set_exempt_rent_epoch_max {
account_expected.set_rent_epoch(RENT_EXEMPT_RENT_EPOCH);
}
assert_eq!(
rent_collector.collect_from_existing_account(
&incinerator::id(),
&mut account_clone,
set_exempt_rent_epoch_max
),
CollectedInfo::default()
);
assert_eq!(account_clone, account_expected);
}
for (rent_epoch, rent_due_expected) in [(2, 2), (3, 5)] {
rent_collector.epoch = rent_epoch;
account.set_lamports(10);
account.set_rent_epoch(1);
let new_rent_epoch_expected = rent_collector.epoch + 1;
assert!(
matches!(
rent_collector.calculate_rent_result(&Pubkey::default(), &account),
RentResult::CollectRent{ new_rent_epoch, rent_due} if new_rent_epoch == new_rent_epoch_expected && rent_due == rent_due_expected,
),
"{:?}",
rent_collector.calculate_rent_result(&Pubkey::default(), &account)
);
{
let mut account_clone = account.clone();
assert_eq!(
rent_collector.collect_from_existing_account(
&Pubkey::default(),
&mut account_clone,
set_exempt_rent_epoch_max
),
CollectedInfo {
rent_amount: rent_due_expected,
account_data_len_reclaimed: 0
}
);
let mut account_expected = account.clone();
account_expected.set_lamports(account.lamports() - rent_due_expected);
account_expected.set_rent_epoch(new_rent_epoch_expected);
assert_eq!(account_clone, account_expected);
}
}
account.set_lamports(1_000_000);
let result = rent_collector.calculate_rent_result(&Pubkey::default(), &account);
assert!(
matches!(result, RentResult::Exempt),
"{result:?}, set_exempt_rent_epoch_max: {set_exempt_rent_epoch_max}",
);
{
let mut account_clone = account.clone();
let mut account_expected = account.clone();
if set_exempt_rent_epoch_max {
account_expected.set_rent_epoch(RENT_EXEMPT_RENT_EPOCH);
}
assert_eq!(
rent_collector.collect_from_existing_account(
&Pubkey::default(),
&mut account_clone,
set_exempt_rent_epoch_max
),
CollectedInfo::default()
);
assert_eq!(account_clone, account_expected);
}
account.set_rent_epoch(1_000_000);
assert_matches!(
rent_collector.calculate_rent_result(&Pubkey::default(), &account),
RentResult::NoRentCollectionNow
);
{
let mut account_clone = account.clone();
assert_eq!(
rent_collector.collect_from_existing_account(
&Pubkey::default(),
&mut account_clone,
set_exempt_rent_epoch_max
),
CollectedInfo::default()
);
assert_eq!(account_clone, account);
}
}
}
#[test]
fn test_collect_from_account_created_and_existing() {
for set_exempt_rent_epoch_max in [false, true] {
let old_lamports = 1000;
let old_epoch = 1;
let new_epoch = 2;
let (mut created_account, mut existing_account) = {
let account = AccountSharedData::from(Account {
lamports: old_lamports,
rent_epoch: old_epoch,
..Account::default()
});
(account.clone(), account)
};
let rent_collector = default_rent_collector_clone_with_epoch(new_epoch);
let collected = rent_collector.collect_from_created_account(
&solana_sdk::pubkey::new_rand(),
&mut created_account,
set_exempt_rent_epoch_max,
);
assert!(created_account.lamports() < old_lamports);
assert_eq!(
created_account.lamports() + collected.rent_amount,
old_lamports
);
assert_ne!(created_account.rent_epoch(), old_epoch);
assert_eq!(collected.account_data_len_reclaimed, 0);
let collected = rent_collector.collect_from_existing_account(
&solana_sdk::pubkey::new_rand(),
&mut existing_account,
set_exempt_rent_epoch_max,
);
assert!(existing_account.lamports() < old_lamports);
assert_eq!(
existing_account.lamports() + collected.rent_amount,
old_lamports
);
assert_ne!(existing_account.rent_epoch(), old_epoch);
assert_eq!(collected.account_data_len_reclaimed, 0);
assert!(created_account.lamports() > existing_account.lamports());
assert_eq!(created_account.rent_epoch(), existing_account.rent_epoch());
}
}
#[test]
fn test_rent_exempt_temporal_escape() {
for set_exempt_rent_epoch_max in [false, true] {
for pass in 0..2 {
let mut account = AccountSharedData::default();
let epoch = 3;
let huge_lamports = 123_456_789_012;
let tiny_lamports = 789_012;
let pubkey = solana_sdk::pubkey::new_rand();
assert_eq!(account.rent_epoch(), 0);
let rent_collector = default_rent_collector_clone_with_epoch(epoch);
if pass == 0 {
account.set_lamports(huge_lamports);
let collected = rent_collector.collect_from_existing_account(
&pubkey,
&mut account,
set_exempt_rent_epoch_max,
);
assert_eq!(account.lamports(), huge_lamports);
assert_eq!(collected, CollectedInfo::default());
continue;
}
account.set_lamports(tiny_lamports);
let collected = rent_collector.collect_from_existing_account(
&pubkey,
&mut account,
set_exempt_rent_epoch_max,
);
assert_eq!(account.lamports(), tiny_lamports - collected.rent_amount);
assert_ne!(collected, CollectedInfo::default());
}
}
}
#[test]
fn test_rent_exempt_sysvar() {
for set_exempt_rent_epoch_max in [false, true] {
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 = default_rent_collector_clone_with_epoch(epoch);
let collected = rent_collector.collect_from_existing_account(
&pubkey,
&mut account,
set_exempt_rent_epoch_max,
);
assert_eq!(account.lamports(), 0);
assert_eq!(collected.rent_amount, 1);
}
}
#[test]
fn test_collect_cleans_up_account() {
for set_exempt_rent_epoch_max in [false, true] {
solana_logger::setup();
let account_lamports = 1; let account_data_len = 567;
let account_rent_epoch = 11;
let mut account = AccountSharedData::from(Account {
lamports: account_lamports, data: vec![u8::default(); account_data_len],
rent_epoch: account_rent_epoch,
..Account::default()
});
let rent_collector = default_rent_collector_clone_with_epoch(account_rent_epoch + 1);
let collected = rent_collector.collect_from_existing_account(
&Pubkey::new_unique(),
&mut account,
set_exempt_rent_epoch_max,
);
assert_eq!(collected.rent_amount, account_lamports);
assert_eq!(
collected.account_data_len_reclaimed,
account_data_len as u64
);
assert_eq!(account, AccountSharedData::default());
}
}
}